├── .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 | [](https://travis-ci.org/kiliankoe/CLISpinner/)
4 |
5 | > 60+ spinners for use in the terminal
6 |
7 | 
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 |
--------------------------------------------------------------------------------