-
-
Notifications
You must be signed in to change notification settings - Fork 56
Expand file tree
/
Copy pathActivityIndicator.swift
More file actions
128 lines (116 loc) · 4.53 KB
/
Copy pathActivityIndicator.swift
File metadata and controls
128 lines (116 loc) · 4.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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
116
117
118
119
120
121
122
123
124
125
126
127
128
import AsyncAlgorithms
import Synchronization
extension ActivityIndicatorType {
/// Creates a new ``ActivityIndicator`` for this ``ActivityIndicatorType``.
///
/// See ``ActivityIndicator`` for more information.
///
/// - parameter console: Console to use for rendering the ``ActivityIndicator``
public func newActivity(for console: any Console) -> ActivityIndicator<Self> {
return .init(activity: self, console: console)
}
}
/// An instance of a ``ActivityIndicatorType`` that can be started, failed, and succeeded.
///
/// Use ``ActivityIndicatorType/newActivity(for:)`` on ``ActivityIndicatorType`` to create one.
///
/// ```swift
/// let loadingBar = console.loadingBar(title: "Loading")
/// try await foo.withActivityIndicator {
/// try await Task.sleep(for: .seconds(2.5))
/// }
/// ```
///
public final class ActivityIndicator<A>: Sendable where A: ActivityIndicatorType {
let _activity: Mutex<A>
/// The generic ``ActivityIndicatorType`` powering this ``ActivityIndicator``.
public var activity: A {
get {
self._activity.withLock { $0 }
}
set {
self._activity.withLock { $0 = newValue }
}
}
/// The ``Console`` this ``ActivityIndicator`` is running on.
private let console: any Console
/// Creates a new ``ActivityIndicator``. Use ``ActivityIndicatorType/newActivity(for:)``.
init(activity: A, console: any Console) {
self.console = console
self._activity = Mutex(activity)
}
/// Starts the ``ActivityIndicator``. Usually this means beginning the associated "loading" animation.
///
/// Once started, ``ActivityIndicator`` will continue to redraw the ``ActivityIndicatorType`` at a fixed
/// refresh rate passing ``ActivityIndicatorState/active``.
///
/// - Parameters:
/// - refreshRate: The time interval (specified in milliseconds) to use
/// when updating the activity.
private func start(refreshRate: Int) async {
guard self.console.supportsANSICommands else {
// Skip animations if the console does not support ANSI commands
self.activity.outputActivityIndicator(to: self.console, state: .ready)
return
}
let timer = AsyncTimerSequence(
interval: .milliseconds(refreshRate),
tolerance: .milliseconds(10),
clock: .suspending
)
var tick: UInt = 0
for await _ in timer {
if tick > 0 {
self.console.popEphemeral()
}
tick = tick &+ 1
self.console.pushEphemeral()
self.activity.outputActivityIndicator(to: self.console, state: .active(tick: tick))
}
}
/// Stops the ``ActivityIndicator``, yielding a failed / error appearance.
///
/// Passes ``ActivityIndicatorState/failure`` to the ``ActivityIndicatorType``.
///
/// Must be called after ``ActivityIndicator/start(refreshRate:)``.
private func fail() {
self.activity.outputActivityIndicator(to: console, state: .failure)
}
/// Stops the ``ActivityIndicator``, yielding a success / done appearance.
///
/// Passes ``ActivityIndicatorState/success`` to the ``ActivityIndicatorType``.
///
/// Must be called after ``ActivityIndicator/start(refreshRate:)``.
private func succeed() {
self.activity.outputActivityIndicator(to: console, state: .success)
}
/// Starts the ``ActivityIndicator`` and stops it after the provided body completes.
///
/// - Parameters:
/// - refreshRate: The time interval (specified in milliseconds) to use when updating the activity.
/// - body: The asynchronous body to execute while the activity indicator is running.
@discardableResult
public func withActivityIndicator<T>(refreshRate: Int = 40, _ body: @Sendable () async throws -> T) async rethrows -> T {
let task = Task {
await self.start(refreshRate: refreshRate)
}
do {
let result = try await body()
task.cancel()
_ = await task.result
if self.console.supportsANSICommands, self.console.depth > 0 {
self.console.popEphemeral()
}
self.succeed()
return result
} catch {
task.cancel()
_ = await task.result
if self.console.supportsANSICommands, self.console.depth > 0 {
self.console.popEphemeral()
}
self.fail()
throw error
}
}
}