├── .gitignore ├── .swift-version ├── .travis.yml ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── CLISpinner │ ├── Pattern.swift │ ├── Patterns+File.swift │ └── Spinner.swift └── Tests ├── CLISpinnerTests ├── CLISpinnerTests.swift ├── grenade.json └── spinners.json └── LinuxMain.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | /Package.resolved 6 | *.*sw[pon] 7 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode10.2 2 | language: objective-c 3 | 4 | script: 5 | - swift build 6 | - swift test 7 | 8 | notifications: 9 | email: 10 | on_success: never 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kilian Koeltzsch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "CLISpinner", 7 | products: [ 8 | .library( 9 | name: "CLISpinner", 10 | targets: ["CLISpinner"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/onevcat/Rainbow", from: "3.1.4"), 14 | ], 15 | targets: [ 16 | .target( 17 | name: "CLISpinner", 18 | dependencies: ["Rainbow"]), 19 | .testTarget( 20 | name: "CLISpinnerTests", 21 | dependencies: ["CLISpinner"]), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLISpinner 2 | 3 | [![Travis](https://img.shields.io/travis/kiliankoe/CLISpinner.svg?style=flat-square)](https://travis-ci.org/kiliankoe/CLISpinner/) 4 | 5 | > 60+ spinners for use in the terminal 6 | 7 | ![](https://github.com/sindresorhus/cli-spinners/raw/master/screenshot.gif) 8 | 9 | Shamelessly ripped off from [sindresorhus/cli-spinners](https://github.com/sindresorhus/cli-spinners). 10 | 11 | 12 | 13 | ## Install 14 | 15 | ```swift 16 | .package(url: "https://github.com/kiliankoe/CLISpinner", from: "see latest release") 17 | ``` 18 | 19 | 20 | 21 | ## Usage 22 | 23 | Just want to display a simple spinner for two seconds? 24 | 25 | ```swift 26 | let s = Spinner(pattern: .dots) 27 | s.start() 28 | sleep(2) 29 | s.stop() 30 | ``` 31 | 32 | Want some changing text and patterns? 33 | 34 | ```swift 35 | let s = Spinner(pattern: .dots, text: "Foobar...", color: .lightCyan) 36 | s.start() 37 | sleep(2) 38 | s.succeed(text: "Barfoo") 39 | // will change the displayed text to '✔ Barfoo' 40 | ``` 41 | 42 | Made your own custom pattern? 43 | 44 | ```swift 45 | let pattern = try Pattern.load(from: "/path/to/your/pattern.json") 46 | let s = spinner(pattern: pattern) 47 | s.start() 48 | sleep(2) 49 | s.stop() 50 | ``` 51 | 52 | Want all the patterns from [sindresorhus/cli-spinners](https://github.com/sindresorhus/cli-spinners/blob/master/spinners.json)? 53 | 54 | ```swift 55 | let patterns = try Patterns(from: "/path/to/spinners.json") 56 | let s = spinner(pattern: patterns["christmas"]!) 57 | s.start() 58 | sleep(2) 59 | s.stop() 60 | ``` 61 | 62 | 63 | 64 | That's basically it 👌 65 | 66 | 67 | 68 | ## Creating your own Pattern 69 | 70 | The `Pattern` type can read in patterns from a JSON file using the following format: 71 | 72 | ```json 73 | { 74 | "frames": [ 75 | "1", 76 | "2", 77 | "3", 78 | "4", 79 | "5" 80 | ], 81 | "speed": 0.08 82 | } 83 | ``` 84 | 85 | To keep multiple patterns in a single file: 86 | 87 | ```json 88 | { 89 | "pattern-name1": { 90 | "frames": [ 91 | "<(**<)", 92 | "<(**)>", 93 | "(>**)>" 94 | ], 95 | "speed": 0.01 96 | }, 97 | "pattern-name2": { 98 | "frames": [ 99 | "1", 100 | "2", 101 | "3", 102 | "2" 103 | ], 104 | "speed": 0.12 105 | } 106 | } 107 | ``` 108 | 109 | 110 | ## Caveat 111 | 112 | To look *nice* the spinner hides the user's cursor as long as it's running and displays it again when stopped. The issue with this is that the cursor will still be hidden if the user interrupts the process (by sending a SIGINT through ctrl+c for example). The best way to handle this is by setting up a signal handler in your code and calling `spinner.unhideCursor()` on exiting. This library purposefully does not do that for you so as not to interfere with any possible signal handlers you might already have set up. 113 | 114 | See [IBM-Swift/BlueSignals](https://github.com/IBM-Swift/BlueSignals) for a clean and safe way of handling signals. The appropriate signal handler for your project could look something like this. 115 | 116 | ```swift 117 | import Signals 118 | 119 | let spinner = Spinner(pattern: .dots) 120 | // ... 121 | 122 | Signals.trap(signal: .int) { _ in 123 | spinner.unhideCursor() 124 | exit(0) 125 | } 126 | ``` 127 | 128 | 129 | 130 | ## Used by 131 | 132 | - [kiliankoe/apodidae](https://github.com/kiliankoe/apodidae) - CLI to search for Swift packages 133 | - [Swift-Watch/Watcher](https://github.com/Swift-Watch/Watcher) - file watcher and test runner for Swift projects 134 | - Your project? 😊 135 | -------------------------------------------------------------------------------- /Sources/CLISpinner/Pattern.swift: -------------------------------------------------------------------------------- 1 | public protocol SpinnerPattern { 2 | var frames: [String] { get } 3 | var speed: Double { get } 4 | } 5 | 6 | // Props go to https://github.com/sindresorhus/cli-spinners 7 | public struct Pattern: SpinnerPattern, Decodable { 8 | public let frames: [String] 9 | public let speed: Double 10 | 11 | public static let dots = Pattern(frames: "⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏", speed: 0.08) 12 | public static let dots2 = Pattern(frames: "⣾","⣽","⣻","⢿","⡿","⣟","⣯","⣷", speed: 0.08) 13 | public static let dots3 = Pattern(frames: "⠋","⠙","⠚","⠞","⠖","⠦","⠴","⠲","⠳","⠓", speed: 0.08) 14 | public static let dots4 = Pattern(frames: "⠄","⠆","⠇","⠋","⠙","⠸","⠰","⠠","⠰","⠸","⠙","⠋","⠇","⠆", speed: 0.08) 15 | public static let dots5 = Pattern(frames: "⠋","⠙","⠚","⠒","⠂","⠂","⠒","⠲","⠴","⠦","⠖","⠒","⠐","⠐","⠒","⠓","⠋", speed: 0.08) 16 | public static let dots6 = Pattern(frames: "⠁","⠉","⠙","⠚","⠒","⠂","⠂","⠒","⠲","⠴","⠤","⠄","⠄","⠤","⠴","⠲","⠒","⠂","⠂","⠒","⠚","⠙","⠉","⠁", speed: 0.08) 17 | public static let dots7 = Pattern(frames: "⠈","⠉","⠋","⠓","⠒","⠐","⠐","⠒","⠖","⠦","⠤","⠠","⠠","⠤","⠦","⠖","⠒","⠐","⠐","⠒","⠓","⠋","⠉","⠈", speed: 0.08) 18 | public static let dots8 = Pattern(frames: "⠁","⠁","⠉","⠙","⠚","⠒","⠂","⠂","⠒","⠲","⠴","⠤","⠄","⠄","⠤","⠠","⠠","⠤","⠦","⠖","⠒","⠐","⠐","⠒","⠓","⠋","⠉","⠈","⠈", speed: 0.08) 19 | public static let dots9 = Pattern(frames: "⢹","⢺","⢼","⣸","⣇","⡧","⡗","⡏", speed: 0.08) 20 | public static let dots10 = Pattern(frames: "⢄","⢂","⢁","⡁","⡈","⡐","⡠", speed: 0.08) 21 | public static let dots11 = Pattern(frames: "⠁","⠂","⠄","⡀","⢀","⠠","⠐","⠈", speed: 0.1) 22 | public static let dots12 = Pattern(frames: "⢀⠀","⡀⠀","⠄⠀","⢂⠀","⡂⠀","⠅⠀","⢃⠀","⡃⠀","⠍⠀","⢋⠀","⡋⠀","⠍⠁","⢋⠁","⡋⠁","⠍⠉","⠋⠉","⠋⠉","⠉⠙","⠉⠙","⠉⠩","⠈⢙","⠈⡙","⢈⠩","⡀⢙","⠄⡙","⢂⠩","⡂⢘","⠅⡘","⢃⠨","⡃⢐","⠍⡐","⢋⠠","⡋⢀","⠍⡁","⢋⠁","⡋⠁","⠍⠉","⠋⠉","⠋⠉","⠉⠙","⠉⠙","⠉⠩","⠈⢙","⠈⡙","⠈⠩","⠀⢙","⠀⡙","⠀⠩","⠀⢘","⠀⡘","⠀⠨","⠀⢐","⠀⡐","⠀⠠","⠀⢀","⠀⡀", speed: 0.08) 23 | 24 | public static let line = Pattern(frames: "-","\\","|","/", speed: 0.13) 25 | public static let line2 = Pattern(frames: "⠂","-","–","—","–","-", speed: 0.1) 26 | 27 | public static let pipe = Pattern(frames: "┤","┘","┴","└","├","┌","┬","┐", speed: 0.1) 28 | 29 | public static let simpleDots = Pattern(frames: ". ",".. ","..."," ", speed: 0.4) 30 | public static let simpleDotsScrolling = Pattern(frames: ". ",".. ","..."," .."," ."," ", speed: 0.2) 31 | 32 | public static let star = Pattern(frames: "✶","✸","✹","✺","✹","✷", speed: 0.7) 33 | public static let star2 = Pattern(frames: "+","x","*", speed: 0.8) 34 | 35 | public static let flip = Pattern(frames: "_","_","_","-","`","`","'","´","-","_","_","_", speed: 0.7) 36 | 37 | public static let hamburger = Pattern(frames: "☱","☲","☴", speed: 0.1) 38 | 39 | public static let growVertical = Pattern(frames: "▁","▃","▄","▅","▆","▇","▆","▅","▄","▃", speed: 0.12) 40 | public static let growHorizontal = Pattern(frames: "▏","▎","▍","▌","▋","▊","▉","▊","▋","▌","▍","▎", speed: 0.12) 41 | 42 | public static let balloon = Pattern(frames: " ",".","o","O","@","*"," ", speed: 0.14) 43 | public static let balloon2 = Pattern(frames: ".","o","O","°","O","o",".", speed: 0.12) 44 | 45 | public static let noise = Pattern(frames: "▓","▒","░", speed: 0.1) 46 | 47 | public static let bounce = Pattern(frames: "⠁","⠂","⠄","⠂", speed: 0.12) 48 | public static let boxBounce = Pattern(frames: "▖","▘","▝","▗", speed: 0.12) 49 | public static let boxBounce2 = Pattern(frames: "▌","▀","▐","▄", speed: 0.1) 50 | 51 | public static let triangle = Pattern(frames: "◢","◣","◤","◥", speed: 0.05) 52 | 53 | public static let arc = Pattern(frames: "◜","◠","◝","◞","◡","◟", speed: 0.1) 54 | public static let circle = Pattern(frames: "◡","⊙","◠", speed: 0.12) 55 | public static let squareCorners = Pattern(frames: "◰","◳","◲","◱", speed: 0.18) 56 | public static let circleQuarters = Pattern(frames: "◴","◷","◶","◵", speed: 0.12) 57 | public static let circleHalves = Pattern(frames: "◐","◓","◑","◒", speed: 0.05) 58 | 59 | public static let squish = Pattern(frames: "╫","╪", speed: 0.1) 60 | 61 | public static let toggle = Pattern(frames: "⊶","⊷", speed: 0.25) 62 | public static let toggle2 = Pattern(frames: "▫","▪", speed: 0.08) 63 | public static let toggle3 = Pattern(frames: "□","■", speed: 0.12) 64 | public static let toggle4 = Pattern(frames: "■","□","▪","▫", speed: 0.1) 65 | public static let toggle5 = Pattern(frames: "▮","▯", speed: 0.1) 66 | public static let toggle6 = Pattern(frames: "ဝ","၀", speed: 0.3) 67 | public static let toggle7 = Pattern(frames: "⦾","⦿", speed: 0.08) 68 | public static let toggle8 = Pattern(frames: "◍","◌", speed: 0.1) 69 | public static let toggle9 = Pattern(frames: "◉","◎", speed: 0.1) 70 | public static let toggle10 = Pattern(frames: "㊂","㊀","㊁", speed: 0.1) 71 | public static let toggle11 = Pattern(frames: "⧇","⧆", speed: 0.1) 72 | public static let toggle12 = Pattern(frames: "☗","☖", speed: 0.12) 73 | public static let toggle13 = Pattern(frames: "=","*","-", speed: 0.08) 74 | 75 | public static let arrow = Pattern(frames: "←","↖","↑","↗","→","↘","↓","↙", speed: 0.1) 76 | public static let arrow2 = Pattern(frames: "⬆️ ","↗️ ","➡️ ","↘️ ","⬇️ ","↙️ ","⬅️ ","↖️ ", speed: 0.08) 77 | public static let arrow3 = Pattern(frames: "▹▹▹▹▹","▸▹▹▹▹","▹▸▹▹▹","▹▹▸▹▹","▹▹▹▸▹","▹▹▹▹▸", speed: 0.12) 78 | 79 | public static let bouncingBar = Pattern(frames: "[ ]","[ =]","[ ==]","[ ===]","[====]","[=== ]","[== ]","[= ]", speed: 0.08) 80 | public static let bouncingBall = Pattern(frames: "( ● )","( ● )","( ● )","( ● )","( ●)","( ● )","( ● )","( ● )","( ● )","(● )", speed: 0.08) 81 | 82 | public static let smiley = Pattern(frames: "😄 ","😝 ", speed: 0.2) 83 | public static let monkey = Pattern(frames: "🙈 ","🙈 ","🙉 ","🙊 ", speed: 0.3) 84 | public static let hearts = Pattern(frames: "💛 ","💙 ","💜 ","💚 ","❤️ ", speed: 0.1) 85 | public static let clock = Pattern(frames: "🕐 ","🕑 ","🕒 ","🕓 ","🕔 ","🕕 ","🕖 ","🕗 ","🕘 ","🕙 ","🕚 ", speed: 0.1) 86 | public static let earth = Pattern(frames: "🌍 ","🌎 ","🌏 ", speed: 0.18) 87 | public static let moon = Pattern(frames: "🌑 ","🌒 ","🌓 ","🌔 ","🌕 ","🌖 ","🌗 ","🌘 ", speed: 0.08) 88 | public static let runner = Pattern(frames: "🚶 ","🏃 ", speed: 0.14) 89 | 90 | public static let pong = Pattern(frames: "▐⠂ ▌","▐⠈ ▌","▐ ⠂ ▌","▐ ⠠ ▌","▐ ⡀ ▌","▐ ⠠ ▌","▐ ⠂ ▌","▐ ⠈ ▌","▐ ⠂ ▌","▐ ⠠ ▌","▐ ⡀ ▌","▐ ⠠ ▌","▐ ⠂ ▌","▐ ⠈ ▌","▐ ⠂▌","▐ ⠠▌","▐ ⡀▌","▐ ⠠ ▌","▐ ⠂ ▌","▐ ⠈ ▌","▐ ⠂ ▌","▐ ⠠ ▌","▐ ⡀ ▌","▐ ⠠ ▌","▐ ⠂ ▌","▐ ⠈ ▌","▐ ⠂ ▌","▐ ⠠ ▌","▐ ⡀ ▌","▐⠠ ▌", speed: 0.08) 91 | 92 | public static let shark = Pattern(frames: "▐|\\____________▌","▐_|\\___________▌","▐__|\\__________▌","▐___|\\_________▌","▐____|\\________▌","▐_____|\\_______▌","▐______|\\______▌","▐_______|\\_____▌","▐________|\\____▌","▐_________|\\___▌","▐__________|\\__▌","▐___________|\\_▌","▐____________|\\▌","▐____________/|▌","▐___________/|_▌","▐__________/|__▌","▐_________/|___▌","▐________/|____▌","▐_______/|_____▌","▐______/|______▌","▐_____/|_______▌","▐____/|________▌","▐___/|_________▌","▐__/|__________▌","▐_/|___________▌","▐/|____________▌", speed: 0.12) 93 | 94 | public static let dqpb = Pattern(frames: "d","q","p","b", speed: 0.1) 95 | 96 | public static func single(_ pattern: String, speed: Double = 1.0) -> Pattern { 97 | return .init(single: pattern) 98 | } 99 | 100 | public static func multiple(_ pattern: String..., speed: Double = 0.08) -> Pattern { 101 | return .multiple(pattern, speed: speed) 102 | } 103 | public static func multiple(_ pattern: [String], speed: Double = 0.08) -> Pattern { 104 | return .init(frames: pattern, speed: speed) 105 | } 106 | 107 | /// Creates a pattern from a single value, e.g. no animation. 108 | /// 109 | /// - Parameter single: the string to show as the spinner 110 | public init(single: String, speed: Double = 1.0) { 111 | self.init(frames: single, speed: speed) 112 | } 113 | 114 | public init(frames: String..., speed: Double) { 115 | self.init(frames: frames, speed: speed) 116 | } 117 | 118 | public init(frames: [String], speed: Double) { 119 | self.frames = frames 120 | self.speed = speed 121 | } 122 | 123 | private enum CodingKeys: String, CodingKey { 124 | case frames, speed, interval 125 | } 126 | 127 | public init(from decoder: Decoder) throws { 128 | let container = try decoder.container(keyedBy: CodingKeys.self) 129 | frames = try container.decode([String].self, forKey: .frames) 130 | 131 | do { 132 | speed = try container.decode(Double.self, forKey: .speed) 133 | } catch DecodingError.keyNotFound { 134 | speed = try container.decode(Double.self, forKey: .interval) / 1000.0 135 | } catch { throw error } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Sources/CLISpinner/Patterns+File.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Data 2 | import class Foundation.FileManager 3 | import class Foundation.JSONDecoder 4 | 5 | 6 | private let manager = FileManager.default 7 | public extension Pattern { 8 | static func load(from filepath: String) throws -> Pattern { 9 | guard manager.fileExists(atPath: filepath) else { 10 | throw PatternFileError.fileDoesNotExist(filepath) 11 | } 12 | guard manager.isReadableFile(atPath: filepath) else { 13 | throw PatternFileError.fileNotReadable(filepath) 14 | } 15 | 16 | guard let data = manager.contents(atPath: filepath) else { 17 | throw PatternFileError.failedToRead(filepath) 18 | } 19 | 20 | return try .load(from: data) 21 | } 22 | 23 | static func load(from data: Data) throws -> Pattern { 24 | return try JSONDecoder().decode(Pattern.self, from: data) 25 | } 26 | } 27 | 28 | public enum PatternFileError: Error { 29 | case fileDoesNotExist(String) 30 | case fileNotReadable(String) 31 | case failedToRead(String) 32 | } 33 | 34 | public struct Patterns: Decodable { 35 | private let patterns: [String: Pattern] 36 | 37 | public init(from filepath: String) throws { 38 | guard manager.fileExists(atPath: filepath) else { 39 | throw PatternFileError.fileDoesNotExist(filepath) 40 | } 41 | guard manager.isReadableFile(atPath: filepath) else { 42 | throw PatternFileError.fileNotReadable(filepath) 43 | } 44 | 45 | guard let data = manager.contents(atPath: filepath) else { 46 | throw PatternFileError.failedToRead(filepath) 47 | } 48 | 49 | self = try .init(from: data) 50 | } 51 | 52 | public init(from data: Data) throws { 53 | self = try JSONDecoder().decode(Patterns.self, from: data) 54 | } 55 | 56 | public init(from decoder: Decoder) throws { 57 | let container = try decoder.singleValueContainer() 58 | patterns = try container.decode([String: Pattern].self) 59 | } 60 | 61 | public subscript(pattern: String) -> Pattern? { 62 | return patterns[pattern] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/CLISpinner/Spinner.swift: -------------------------------------------------------------------------------- 1 | import func Foundation.fflush 2 | import let Foundation.stdout 3 | import Rainbow 4 | import Dispatch 5 | 6 | public final class Spinner { 7 | /// The pattern the spinner uses. 8 | public var frames: [String] { 9 | willSet { 10 | frameIdx = newValue.startIndex 11 | } 12 | } 13 | /// The time to wait in seconds between each frame of the animation. 14 | public var speed: Double 15 | /// Text that is displayed right next to the spinner. 16 | public var text: String { 17 | get { 18 | return _text 19 | } 20 | set { 21 | let (_, _, _, newText) = Rainbow.extractModes(for: newValue) 22 | let (_, _, _, oldText) = Rainbow.extractModes(for: _text) 23 | let diff = oldText.count - newText.count 24 | if diff > 0 { 25 | _text = newValue 26 | _text += Array(repeating: " ", count: diff) 27 | } else { 28 | _text = newValue 29 | } 30 | } 31 | } 32 | 33 | private var _text = "" 34 | public private(set) var isRunning = false 35 | private var frameIdx: Array.Index! 36 | private let queue = DispatchQueue(label: "io.kilian.CLISpinner") 37 | 38 | /// Create a new `Spinner`. 39 | /// 40 | /// - Parameters: 41 | /// - pattern: The pattern to use. 42 | /// - text: Text to display, defaults to none. 43 | /// - speed: Custom speed value, defaults to a recommended value for each predefined pattern. 44 | /// - color: Custom spinner color, defaults to .default. 45 | public init(pattern: SpinnerPattern, text: String = "", speed: Double? = nil, color: Color = .default) { 46 | frames = pattern.frames.map { $0.applyingColor(color) } 47 | _text = text 48 | self.speed = speed ?? pattern.speed 49 | frameIdx = frames.startIndex 50 | } 51 | 52 | /// Create a new `Spinner`. 53 | /// 54 | /// - Parameters: 55 | /// - pattern: The pattern to use. 56 | /// - text: Text to display, defaults to none. 57 | /// - speed: Custom speed value, defaults to a recommended value for each predefined pattern. 58 | /// - color: Custom spinner color, defaults to .default. 59 | public convenience init(pattern: Pattern, text: String = "", speed: Double? = nil, color: Color = .default) { 60 | self.init(pattern: pattern as SpinnerPattern, text: text, speed: speed, color: color) 61 | } 62 | 63 | /// Start the spinner. 64 | public func start() { 65 | guard !isRunning else { return } 66 | hideCursor(true) 67 | isRunning = true 68 | 69 | queue.async { [weak self] in 70 | guard let `self` = self else { return } 71 | self.renderForever() 72 | } 73 | } 74 | 75 | private func renderForever() { 76 | guard isRunning else { return } 77 | render() 78 | queue.asyncAfter(deadline: .now() + speed) { [weak self] in 79 | guard let `self` = self else { return } 80 | self.renderForever() 81 | } 82 | } 83 | 84 | /// Stop the spinner. 85 | /// 86 | /// - Parameters: 87 | /// - text: Text to display as a final value when stopping. 88 | /// - symbol: A symbol to replace the spinner with when stopping. 89 | /// - terminator: The string to print after stopping. Defaults to newline ("\n"). 90 | public func stop(text: String? = nil, symbol: String? = nil, terminator: String = "\n") { 91 | guard isRunning else { return } 92 | 93 | if let text = text { 94 | self.text = text 95 | } 96 | if let symbol = symbol { 97 | frames = [symbol.isEmpty ? " " : symbol] 98 | } 99 | render() 100 | isRunning = false 101 | hideCursor(false) 102 | print(terminator: terminator) 103 | } 104 | 105 | /// Stop the spinner and remove it entirely. 106 | public func stopAndClear() { 107 | guard isRunning else { return } 108 | 109 | stop(text: "", symbol: " ", terminator: "") 110 | output("\r") 111 | } 112 | 113 | /// Stop the spinner, change it to a green '✔' and persist the current or provided text. 114 | /// 115 | /// - Parameter text: Text to persist if not the one already set 116 | public func succeed(text: String? = nil) { 117 | stop(text: text, symbol: "✔".green) 118 | } 119 | 120 | /// Stop the spinner, change it to a red '✖' and persist the current or provided text. 121 | /// 122 | /// - Parameter text: Text to persist if not the one already set 123 | public func fail(text: String? = nil) { 124 | stop(text: text, symbol: "✖".red) 125 | } 126 | 127 | /// Stop the spinner, change it to a yellow '⚠' and persist the current or provided text. 128 | /// 129 | /// - Parameter text: Text to persist if not the one already set 130 | public func warn(text: String? = nil) { 131 | stop(text: text, symbol: "⚠".yellow) 132 | } 133 | 134 | /// Stop the spinner, change it to a blue 'ℹ' and persist the current or provided text. 135 | /// 136 | /// - Parameter text: Text to persist if not the one already set 137 | public func info(text: String? = nil) { 138 | stop(text: text, symbol: "ℹ".blue) 139 | } 140 | 141 | func frame() -> String { 142 | let frame = frames[frameIdx] 143 | frameIdx = (frameIdx + 1) % frames.count 144 | return "\(frame) \(_text)" 145 | } 146 | 147 | private func resetCursor() { 148 | print("\r", terminator: "") 149 | } 150 | 151 | private func render() { 152 | resetCursor() 153 | output(frame()) 154 | } 155 | 156 | private func output(_ value: String) { 157 | print(value, terminator: "") 158 | fflush(stdout) // necessary for the carriage return in start() 159 | } 160 | 161 | private func hideCursor(_ hide: Bool) { 162 | if hide { 163 | output("\u{001B}[?25l") 164 | } else { 165 | output("\u{001B}[?25h") 166 | } 167 | } 168 | 169 | /// Unhide the cursor. 170 | /// 171 | /// - Note: This should most definitely be called on a SIGINT in your project. 172 | public func unhideCursor() { 173 | hideCursor(false) 174 | } 175 | 176 | deinit { 177 | unhideCursor() 178 | } 179 | } 180 | 181 | -------------------------------------------------------------------------------- /Tests/CLISpinnerTests/CLISpinnerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import class Foundation.NSRegularExpression 3 | import func Foundation.NSMakeRange 4 | @testable import CLISpinner 5 | 6 | class CLISpinnerTests: XCTestCase { 7 | 8 | func testState() { 9 | let s = Spinner(pattern: .dots, text: "start text") 10 | s.start() 11 | 12 | XCTAssertEqual(s.isRunning, true) 13 | XCTAssertEqual(s.text, "start text") 14 | 15 | s.stop(text: "stop text", symbol: "S") 16 | 17 | XCTAssertEqual(s.isRunning, false) 18 | XCTAssertEqual(s.text, "stop text ") 19 | XCTAssertEqual(s.frames, ["S"]) 20 | } 21 | 22 | func testText() { 23 | let s = Spinner(pattern: .dots) 24 | s.text = "foobar" 25 | XCTAssertEqual(s.text, "foobar") 26 | s.text = "something longer" 27 | XCTAssertEqual(s.text, "something longer") 28 | s.text = "shorter" 29 | XCTAssertEqual(s.text, "shorter ") 30 | } 31 | 32 | func testFrame() { 33 | let p = Pattern.multiple(["a", "b", "c"]) 34 | let s = Spinner(pattern: p) 35 | 36 | XCTAssertEqual(s.frame().cleanString(), "a ") 37 | XCTAssertEqual(s.frame().cleanString(), "b ") 38 | XCTAssertEqual(s.frame().cleanString(), "c ") 39 | XCTAssertEqual(s.frame().cleanString(), "a ") 40 | } 41 | 42 | // func testExample() { 43 | // let s = Spinner(pattern: .dots, text: "Searching...", color: .lightCyan) 44 | // s.start() 45 | // sleep(2) 46 | // s.succeed(text: "Found 5 results") 47 | // print() 48 | // } 49 | 50 | func testLoadPattern() { 51 | let path = #file.components(separatedBy: "/").dropLast().joined(separator: "/") + "/grenade.json" 52 | let pattern: CLISpinner.Pattern 53 | do { 54 | pattern = try .load(from: path) 55 | } catch { 56 | XCTFail("Failed to initialize Patterns from \(path) with error: \(type(of: error)).\(error)") 57 | return 58 | } 59 | 60 | let s = Spinner(pattern: pattern) 61 | XCTAssertEqual(s.frames.map({ $0.cleanString() }), ["، ", "′ ", " ´ ", " ‾ ", " ⸌", " ⸊", " |", " ⁎", " ⁕", " ෴ ", " ⁓", " "," ", " "]) 62 | XCTAssertEqual(s.speed, 0.08) 63 | } 64 | 65 | func testLoadPatterns() { 66 | let path = #file.components(separatedBy: "/").dropLast().joined(separator: "/") + "/spinners.json" 67 | let patterns: Patterns 68 | do { 69 | patterns = try .init(from: path) 70 | } catch { 71 | XCTFail("Failed to initialize Patterns from \(path) with error: \(type(of: error)).\(error)") 72 | return 73 | } 74 | 75 | let s = Spinner(pattern: patterns["layer"]!) 76 | XCTAssertEqual(s.frames.map({ $0.cleanString() }), ["-", "=", "≡"]) 77 | XCTAssertEqual(s.speed, 0.15) 78 | } 79 | 80 | static var allTests = [ 81 | ("testState", testState), 82 | ("testText", testText), 83 | ("testFrame", testFrame), 84 | ("testLoadPattern", testLoadPattern), 85 | ("testLoadPatterns", testLoadPatterns), 86 | ] 87 | } 88 | 89 | private var cleanRegex = try! NSRegularExpression(pattern: "(\u{009B}|\u{001B})\\[[0-?]*[ -\\/]*[@-~]", options: .caseInsensitive) 90 | fileprivate extension String { 91 | func cleanString() -> String? { 92 | return cleanRegex.stringByReplacingMatches(in: self, range: NSMakeRange(0, count), withTemplate: "") 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/CLISpinnerTests/grenade.json: -------------------------------------------------------------------------------- 1 | { 2 | "interval": 80, 3 | "frames": [ 4 | "، ", 5 | "′ ", 6 | " ´ ", 7 | " ‾ ", 8 | " ⸌", 9 | " ⸊", 10 | " |", 11 | " ⁎", 12 | " ⁕", 13 | " ෴ ", 14 | " ⁓", 15 | " ", 16 | " ", 17 | " " 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /Tests/CLISpinnerTests/spinners.json: -------------------------------------------------------------------------------- 1 | { 2 | "dots": { 3 | "interval": 80, 4 | "frames": [ 5 | "⠋", 6 | "⠙", 7 | "⠹", 8 | "⠸", 9 | "⠼", 10 | "⠴", 11 | "⠦", 12 | "⠧", 13 | "⠇", 14 | "⠏" 15 | ] 16 | }, 17 | "dots2": { 18 | "interval": 80, 19 | "frames": [ 20 | "⣾", 21 | "⣽", 22 | "⣻", 23 | "⢿", 24 | "⡿", 25 | "⣟", 26 | "⣯", 27 | "⣷" 28 | ] 29 | }, 30 | "dots3": { 31 | "interval": 80, 32 | "frames": [ 33 | "⠋", 34 | "⠙", 35 | "⠚", 36 | "⠞", 37 | "⠖", 38 | "⠦", 39 | "⠴", 40 | "⠲", 41 | "⠳", 42 | "⠓" 43 | ] 44 | }, 45 | "dots4": { 46 | "interval": 80, 47 | "frames": [ 48 | "⠄", 49 | "⠆", 50 | "⠇", 51 | "⠋", 52 | "⠙", 53 | "⠸", 54 | "⠰", 55 | "⠠", 56 | "⠰", 57 | "⠸", 58 | "⠙", 59 | "⠋", 60 | "⠇", 61 | "⠆" 62 | ] 63 | }, 64 | "dots5": { 65 | "interval": 80, 66 | "frames": [ 67 | "⠋", 68 | "⠙", 69 | "⠚", 70 | "⠒", 71 | "⠂", 72 | "⠂", 73 | "⠒", 74 | "⠲", 75 | "⠴", 76 | "⠦", 77 | "⠖", 78 | "⠒", 79 | "⠐", 80 | "⠐", 81 | "⠒", 82 | "⠓", 83 | "⠋" 84 | ] 85 | }, 86 | "dots6": { 87 | "interval": 80, 88 | "frames": [ 89 | "⠁", 90 | "⠉", 91 | "⠙", 92 | "⠚", 93 | "⠒", 94 | "⠂", 95 | "⠂", 96 | "⠒", 97 | "⠲", 98 | "⠴", 99 | "⠤", 100 | "⠄", 101 | "⠄", 102 | "⠤", 103 | "⠴", 104 | "⠲", 105 | "⠒", 106 | "⠂", 107 | "⠂", 108 | "⠒", 109 | "⠚", 110 | "⠙", 111 | "⠉", 112 | "⠁" 113 | ] 114 | }, 115 | "dots7": { 116 | "interval": 80, 117 | "frames": [ 118 | "⠈", 119 | "⠉", 120 | "⠋", 121 | "⠓", 122 | "⠒", 123 | "⠐", 124 | "⠐", 125 | "⠒", 126 | "⠖", 127 | "⠦", 128 | "⠤", 129 | "⠠", 130 | "⠠", 131 | "⠤", 132 | "⠦", 133 | "⠖", 134 | "⠒", 135 | "⠐", 136 | "⠐", 137 | "⠒", 138 | "⠓", 139 | "⠋", 140 | "⠉", 141 | "⠈" 142 | ] 143 | }, 144 | "dots8": { 145 | "interval": 80, 146 | "frames": [ 147 | "⠁", 148 | "⠁", 149 | "⠉", 150 | "⠙", 151 | "⠚", 152 | "⠒", 153 | "⠂", 154 | "⠂", 155 | "⠒", 156 | "⠲", 157 | "⠴", 158 | "⠤", 159 | "⠄", 160 | "⠄", 161 | "⠤", 162 | "⠠", 163 | "⠠", 164 | "⠤", 165 | "⠦", 166 | "⠖", 167 | "⠒", 168 | "⠐", 169 | "⠐", 170 | "⠒", 171 | "⠓", 172 | "⠋", 173 | "⠉", 174 | "⠈", 175 | "⠈" 176 | ] 177 | }, 178 | "dots9": { 179 | "interval": 80, 180 | "frames": [ 181 | "⢹", 182 | "⢺", 183 | "⢼", 184 | "⣸", 185 | "⣇", 186 | "⡧", 187 | "⡗", 188 | "⡏" 189 | ] 190 | }, 191 | "dots10": { 192 | "interval": 80, 193 | "frames": [ 194 | "⢄", 195 | "⢂", 196 | "⢁", 197 | "⡁", 198 | "⡈", 199 | "⡐", 200 | "⡠" 201 | ] 202 | }, 203 | "dots11": { 204 | "interval": 100, 205 | "frames": [ 206 | "⠁", 207 | "⠂", 208 | "⠄", 209 | "⡀", 210 | "⢀", 211 | "⠠", 212 | "⠐", 213 | "⠈" 214 | ] 215 | }, 216 | "dots12": { 217 | "interval": 80, 218 | "frames": [ 219 | "⢀⠀", 220 | "⡀⠀", 221 | "⠄⠀", 222 | "⢂⠀", 223 | "⡂⠀", 224 | "⠅⠀", 225 | "⢃⠀", 226 | "⡃⠀", 227 | "⠍⠀", 228 | "⢋⠀", 229 | "⡋⠀", 230 | "⠍⠁", 231 | "⢋⠁", 232 | "⡋⠁", 233 | "⠍⠉", 234 | "⠋⠉", 235 | "⠋⠉", 236 | "⠉⠙", 237 | "⠉⠙", 238 | "⠉⠩", 239 | "⠈⢙", 240 | "⠈⡙", 241 | "⢈⠩", 242 | "⡀⢙", 243 | "⠄⡙", 244 | "⢂⠩", 245 | "⡂⢘", 246 | "⠅⡘", 247 | "⢃⠨", 248 | "⡃⢐", 249 | "⠍⡐", 250 | "⢋⠠", 251 | "⡋⢀", 252 | "⠍⡁", 253 | "⢋⠁", 254 | "⡋⠁", 255 | "⠍⠉", 256 | "⠋⠉", 257 | "⠋⠉", 258 | "⠉⠙", 259 | "⠉⠙", 260 | "⠉⠩", 261 | "⠈⢙", 262 | "⠈⡙", 263 | "⠈⠩", 264 | "⠀⢙", 265 | "⠀⡙", 266 | "⠀⠩", 267 | "⠀⢘", 268 | "⠀⡘", 269 | "⠀⠨", 270 | "⠀⢐", 271 | "⠀⡐", 272 | "⠀⠠", 273 | "⠀⢀", 274 | "⠀⡀" 275 | ] 276 | }, 277 | "line": { 278 | "interval": 130, 279 | "frames": [ 280 | "-", 281 | "\\", 282 | "|", 283 | "/" 284 | ] 285 | }, 286 | "line2": { 287 | "interval": 100, 288 | "frames": [ 289 | "⠂", 290 | "-", 291 | "–", 292 | "—", 293 | "–", 294 | "-" 295 | ] 296 | }, 297 | "pipe": { 298 | "interval": 100, 299 | "frames": [ 300 | "┤", 301 | "┘", 302 | "┴", 303 | "└", 304 | "├", 305 | "┌", 306 | "┬", 307 | "┐" 308 | ] 309 | }, 310 | "simpleDots": { 311 | "interval": 400, 312 | "frames": [ 313 | ". ", 314 | ".. ", 315 | "...", 316 | " " 317 | ] 318 | }, 319 | "simpleDotsScrolling": { 320 | "interval": 200, 321 | "frames": [ 322 | ". ", 323 | ".. ", 324 | "...", 325 | " ..", 326 | " .", 327 | " " 328 | ] 329 | }, 330 | "star": { 331 | "interval": 70, 332 | "frames": [ 333 | "✶", 334 | "✸", 335 | "✹", 336 | "✺", 337 | "✹", 338 | "✷" 339 | ] 340 | }, 341 | "star2": { 342 | "interval": 80, 343 | "frames": [ 344 | "+", 345 | "x", 346 | "*" 347 | ] 348 | }, 349 | "flip": { 350 | "interval": 70, 351 | "frames": [ 352 | "_", 353 | "_", 354 | "_", 355 | "-", 356 | "`", 357 | "`", 358 | "'", 359 | "´", 360 | "-", 361 | "_", 362 | "_", 363 | "_" 364 | ] 365 | }, 366 | "hamburger": { 367 | "interval": 100, 368 | "frames": [ 369 | "☱", 370 | "☲", 371 | "☴" 372 | ] 373 | }, 374 | "growVertical": { 375 | "interval": 120, 376 | "frames": [ 377 | "▁", 378 | "▃", 379 | "▄", 380 | "▅", 381 | "▆", 382 | "▇", 383 | "▆", 384 | "▅", 385 | "▄", 386 | "▃" 387 | ] 388 | }, 389 | "growHorizontal": { 390 | "interval": 120, 391 | "frames": [ 392 | "▏", 393 | "▎", 394 | "▍", 395 | "▌", 396 | "▋", 397 | "▊", 398 | "▉", 399 | "▊", 400 | "▋", 401 | "▌", 402 | "▍", 403 | "▎" 404 | ] 405 | }, 406 | "balloon": { 407 | "interval": 140, 408 | "frames": [ 409 | " ", 410 | ".", 411 | "o", 412 | "O", 413 | "@", 414 | "*", 415 | " " 416 | ] 417 | }, 418 | "balloon2": { 419 | "interval": 120, 420 | "frames": [ 421 | ".", 422 | "o", 423 | "O", 424 | "°", 425 | "O", 426 | "o", 427 | "." 428 | ] 429 | }, 430 | "noise": { 431 | "interval": 100, 432 | "frames": [ 433 | "▓", 434 | "▒", 435 | "░" 436 | ] 437 | }, 438 | "bounce": { 439 | "interval": 120, 440 | "frames": [ 441 | "⠁", 442 | "⠂", 443 | "⠄", 444 | "⠂" 445 | ] 446 | }, 447 | "boxBounce": { 448 | "interval": 120, 449 | "frames": [ 450 | "▖", 451 | "▘", 452 | "▝", 453 | "▗" 454 | ] 455 | }, 456 | "boxBounce2": { 457 | "interval": 100, 458 | "frames": [ 459 | "▌", 460 | "▀", 461 | "▐", 462 | "▄" 463 | ] 464 | }, 465 | "triangle": { 466 | "interval": 50, 467 | "frames": [ 468 | "◢", 469 | "◣", 470 | "◤", 471 | "◥" 472 | ] 473 | }, 474 | "arc": { 475 | "interval": 100, 476 | "frames": [ 477 | "◜", 478 | "◠", 479 | "◝", 480 | "◞", 481 | "◡", 482 | "◟" 483 | ] 484 | }, 485 | "circle": { 486 | "interval": 120, 487 | "frames": [ 488 | "◡", 489 | "⊙", 490 | "◠" 491 | ] 492 | }, 493 | "squareCorners": { 494 | "interval": 180, 495 | "frames": [ 496 | "◰", 497 | "◳", 498 | "◲", 499 | "◱" 500 | ] 501 | }, 502 | "circleQuarters": { 503 | "interval": 120, 504 | "frames": [ 505 | "◴", 506 | "◷", 507 | "◶", 508 | "◵" 509 | ] 510 | }, 511 | "circleHalves": { 512 | "interval": 50, 513 | "frames": [ 514 | "◐", 515 | "◓", 516 | "◑", 517 | "◒" 518 | ] 519 | }, 520 | "squish": { 521 | "interval": 100, 522 | "frames": [ 523 | "╫", 524 | "╪" 525 | ] 526 | }, 527 | "toggle": { 528 | "interval": 250, 529 | "frames": [ 530 | "⊶", 531 | "⊷" 532 | ] 533 | }, 534 | "toggle2": { 535 | "interval": 80, 536 | "frames": [ 537 | "▫", 538 | "▪" 539 | ] 540 | }, 541 | "toggle3": { 542 | "interval": 120, 543 | "frames": [ 544 | "□", 545 | "■" 546 | ] 547 | }, 548 | "toggle4": { 549 | "interval": 100, 550 | "frames": [ 551 | "■", 552 | "□", 553 | "▪", 554 | "▫" 555 | ] 556 | }, 557 | "toggle5": { 558 | "interval": 100, 559 | "frames": [ 560 | "▮", 561 | "▯" 562 | ] 563 | }, 564 | "toggle6": { 565 | "interval": 300, 566 | "frames": [ 567 | "ဝ", 568 | "၀" 569 | ] 570 | }, 571 | "toggle7": { 572 | "interval": 80, 573 | "frames": [ 574 | "⦾", 575 | "⦿" 576 | ] 577 | }, 578 | "toggle8": { 579 | "interval": 100, 580 | "frames": [ 581 | "◍", 582 | "◌" 583 | ] 584 | }, 585 | "toggle9": { 586 | "interval": 100, 587 | "frames": [ 588 | "◉", 589 | "◎" 590 | ] 591 | }, 592 | "toggle10": { 593 | "interval": 100, 594 | "frames": [ 595 | "㊂", 596 | "㊀", 597 | "㊁" 598 | ] 599 | }, 600 | "toggle11": { 601 | "interval": 50, 602 | "frames": [ 603 | "⧇", 604 | "⧆" 605 | ] 606 | }, 607 | "toggle12": { 608 | "interval": 120, 609 | "frames": [ 610 | "☗", 611 | "☖" 612 | ] 613 | }, 614 | "toggle13": { 615 | "interval": 80, 616 | "frames": [ 617 | "=", 618 | "*", 619 | "-" 620 | ] 621 | }, 622 | "arrow": { 623 | "interval": 100, 624 | "frames": [ 625 | "←", 626 | "↖", 627 | "↑", 628 | "↗", 629 | "→", 630 | "↘", 631 | "↓", 632 | "↙" 633 | ] 634 | }, 635 | "arrow2": { 636 | "interval": 80, 637 | "frames": [ 638 | "⬆️ ", 639 | "↗️ ", 640 | "➡️ ", 641 | "↘️ ", 642 | "⬇️ ", 643 | "↙️ ", 644 | "⬅️ ", 645 | "↖️ " 646 | ] 647 | }, 648 | "arrow3": { 649 | "interval": 120, 650 | "frames": [ 651 | "▹▹▹▹▹", 652 | "▸▹▹▹▹", 653 | "▹▸▹▹▹", 654 | "▹▹▸▹▹", 655 | "▹▹▹▸▹", 656 | "▹▹▹▹▸" 657 | ] 658 | }, 659 | "bouncingBar": { 660 | "interval": 80, 661 | "frames": [ 662 | "[ ]", 663 | "[= ]", 664 | "[== ]", 665 | "[=== ]", 666 | "[ ===]", 667 | "[ ==]", 668 | "[ =]", 669 | "[ ]", 670 | "[ =]", 671 | "[ ==]", 672 | "[ ===]", 673 | "[====]", 674 | "[=== ]", 675 | "[== ]", 676 | "[= ]" 677 | ] 678 | }, 679 | "bouncingBall": { 680 | "interval": 80, 681 | "frames": [ 682 | "( ● )", 683 | "( ● )", 684 | "( ● )", 685 | "( ● )", 686 | "( ●)", 687 | "( ● )", 688 | "( ● )", 689 | "( ● )", 690 | "( ● )", 691 | "(● )" 692 | ] 693 | }, 694 | "smiley": { 695 | "interval": 200, 696 | "frames": [ 697 | "😄 ", 698 | "😝 " 699 | ] 700 | }, 701 | "monkey": { 702 | "interval": 300, 703 | "frames": [ 704 | "🙈 ", 705 | "🙈 ", 706 | "🙉 ", 707 | "🙊 " 708 | ] 709 | }, 710 | "hearts": { 711 | "interval": 100, 712 | "frames": [ 713 | "💛 ", 714 | "💙 ", 715 | "💜 ", 716 | "💚 ", 717 | "❤️ " 718 | ] 719 | }, 720 | "clock": { 721 | "interval": 100, 722 | "frames": [ 723 | "🕛 ", 724 | "🕐 ", 725 | "🕑 ", 726 | "🕒 ", 727 | "🕓 ", 728 | "🕔 ", 729 | "🕕 ", 730 | "🕖 ", 731 | "🕗 ", 732 | "🕘 ", 733 | "🕙 ", 734 | "🕚 " 735 | ] 736 | }, 737 | "earth": { 738 | "interval": 180, 739 | "frames": [ 740 | "🌍 ", 741 | "🌎 ", 742 | "🌏 " 743 | ] 744 | }, 745 | "moon": { 746 | "interval": 80, 747 | "frames": [ 748 | "🌑 ", 749 | "🌒 ", 750 | "🌓 ", 751 | "🌔 ", 752 | "🌕 ", 753 | "🌖 ", 754 | "🌗 ", 755 | "🌘 " 756 | ] 757 | }, 758 | "runner": { 759 | "interval": 140, 760 | "frames": [ 761 | "🚶 ", 762 | "🏃 " 763 | ] 764 | }, 765 | "pong": { 766 | "interval": 80, 767 | "frames": [ 768 | "▐⠂ ▌", 769 | "▐⠈ ▌", 770 | "▐ ⠂ ▌", 771 | "▐ ⠠ ▌", 772 | "▐ ⡀ ▌", 773 | "▐ ⠠ ▌", 774 | "▐ ⠂ ▌", 775 | "▐ ⠈ ▌", 776 | "▐ ⠂ ▌", 777 | "▐ ⠠ ▌", 778 | "▐ ⡀ ▌", 779 | "▐ ⠠ ▌", 780 | "▐ ⠂ ▌", 781 | "▐ ⠈ ▌", 782 | "▐ ⠂▌", 783 | "▐ ⠠▌", 784 | "▐ ⡀▌", 785 | "▐ ⠠ ▌", 786 | "▐ ⠂ ▌", 787 | "▐ ⠈ ▌", 788 | "▐ ⠂ ▌", 789 | "▐ ⠠ ▌", 790 | "▐ ⡀ ▌", 791 | "▐ ⠠ ▌", 792 | "▐ ⠂ ▌", 793 | "▐ ⠈ ▌", 794 | "▐ ⠂ ▌", 795 | "▐ ⠠ ▌", 796 | "▐ ⡀ ▌", 797 | "▐⠠ ▌" 798 | ] 799 | }, 800 | "shark": { 801 | "interval": 120, 802 | "frames": [ 803 | "▐|\\____________▌", 804 | "▐_|\\___________▌", 805 | "▐__|\\__________▌", 806 | "▐___|\\_________▌", 807 | "▐____|\\________▌", 808 | "▐_____|\\_______▌", 809 | "▐______|\\______▌", 810 | "▐_______|\\_____▌", 811 | "▐________|\\____▌", 812 | "▐_________|\\___▌", 813 | "▐__________|\\__▌", 814 | "▐___________|\\_▌", 815 | "▐____________|\\▌", 816 | "▐____________/|▌", 817 | "▐___________/|_▌", 818 | "▐__________/|__▌", 819 | "▐_________/|___▌", 820 | "▐________/|____▌", 821 | "▐_______/|_____▌", 822 | "▐______/|______▌", 823 | "▐_____/|_______▌", 824 | "▐____/|________▌", 825 | "▐___/|_________▌", 826 | "▐__/|__________▌", 827 | "▐_/|___________▌", 828 | "▐/|____________▌" 829 | ] 830 | }, 831 | "dqpb": { 832 | "interval": 100, 833 | "frames": [ 834 | "d", 835 | "q", 836 | "p", 837 | "b" 838 | ] 839 | }, 840 | "weather": { 841 | "interval": 100, 842 | "frames": [ 843 | "☀️ ", 844 | "☀️ ", 845 | "☀️ ", 846 | "🌤 ", 847 | "⛅️ ", 848 | "🌥 ", 849 | "☁️ ", 850 | "🌧 ", 851 | "🌨 ", 852 | "🌧 ", 853 | "🌨 ", 854 | "🌧 ", 855 | "🌨 ", 856 | "⛈ ", 857 | "🌨 ", 858 | "🌧 ", 859 | "🌨 ", 860 | "☁️ ", 861 | "🌥 ", 862 | "⛅️ ", 863 | "🌤 ", 864 | "☀️ ", 865 | "☀️ " 866 | ] 867 | }, 868 | "christmas": { 869 | "interval": 400, 870 | "frames": [ 871 | "🌲", 872 | "🎄" 873 | ] 874 | }, 875 | "grenade": { 876 | "interval": 80, 877 | "frames": [ 878 | "، ", 879 | "′ ", 880 | " ´ ", 881 | " ‾ ", 882 | " ⸌", 883 | " ⸊", 884 | " |", 885 | " ⁎", 886 | " ⁕", 887 | " ෴ ", 888 | " ⁓", 889 | " ", 890 | " ", 891 | " " 892 | ] 893 | }, 894 | "point": { 895 | "interval": 125, 896 | "frames": [ 897 | "∙∙∙", 898 | "●∙∙", 899 | "∙●∙", 900 | "∙∙●", 901 | "∙∙∙" 902 | ] 903 | }, 904 | "layer": { 905 | "interval": 150, 906 | "frames": [ 907 | "-", 908 | "=", 909 | "≡" 910 | ] 911 | } 912 | } 913 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import CLISpinnerTests 3 | 4 | XCTMain([ 5 | testCase(CLISpinnerTests.allTests), 6 | ]) 7 | --------------------------------------------------------------------------------