Skip to content

Commit a9475ad

Browse files
authored
Merge pull request #37 from pmonks/dev
Release 2.0.279
2 parents 4b2241c + 6ac87d8 commit a9475ad

File tree

8 files changed

+106
-94
lines changed

8 files changed

+106
-94
lines changed

README.md

Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
[![License](https://img.shields.io/github/license/pmonks/spinner.svg)](https://github.com/pmonks/spinner/blob/release/LICENSE)
99
![Maintained](https://badges.ws/badge/?label=maintained&value=yes,+at+author's+discretion)
1010

11-
Progress indicators for command line Clojure apps, including support for indeterminate tasks (those where progress cannot be measured) and determinate tasks (those where progress can be measured). The former are represented using "spinners", while the latter are represented using "progress bars".
11+
Progress indicators for command line Clojure apps, including support for indeterminate tasks (those where progress _cannot_ be measured) and determinate tasks (those where progress _can_ be measured). The former are represented using "spinners", while the latter are represented using "progress bars".
1212

13-
## What is it useful for?
13+
#### Why?
1414

1515
To give the user of a command line app a visual progress indicator during long running processes.
1616

17-
Here it is in action (from the unit tests):
17+
Here it is in action (from the [demo script](https://github.com/pmonks/spinner/blob/dev/demo.clj)):
18+
1819
<p align="center">
1920
<img alt="Spinner example screenshot" src="https://raw.githubusercontent.com/pmonks/spinner/dev/spinner-demo.gif"/>
2021
</p>
@@ -25,6 +26,10 @@ Note that using Unicode characters in progress indicators may be unreliable, dep
2526

2627
`spinner` is available as a Maven artifact from [Clojars](https://clojars.org/com.github.pmonks/spinner).
2728

29+
## Usage
30+
31+
[API documentation is available here](https://pmonks.github.io/spinner/). The [unit](https://github.com/pmonks/spinner/blob/release/test/progress/indeterminate_test.clj) [tests](https://github.com/pmonks/spinner/blob/release/test/progress/determinate_test.clj) provide comprehensive usage examples (alternative animation sets, formatting, etc.).
32+
2833
### Trying it Out
2934

3035
**Important Notes:**
@@ -49,56 +54,32 @@ $ lein trampoline try com.github.pmonks/spinner
4954

5055
Doesn't work properly, for the same reason the `clj` command line doesn't work properly (`rlwrap` intercepts the ANSI escape sequences emitted by this library and misinterprets them).
5156

52-
#### Simple REPL Session
53-
54-
##### Indeterminate Task (aka "spinner")
57+
### [Demo](https://github.com/pmonks/spinner/blob/dev/demo.clj)
5558

5659
```clojure
57-
(require '[progress.indeterminate :as pi] :reload-all)
58-
59-
(pi/animate!
60-
(pi/print "A long running process...")
61-
(Thread/sleep 2500) ; Simulate a long running process
62-
(pi/print "\nAnother long running process...")
63-
(Thread/sleep 2500) ; Simulate another long running process
64-
(pi/print "\nAll done!\n"))
65-
```
60+
;; Indeterminate Task (aka "spinner")
6661

67-
##### Determinate Task (aka "progress bar")
62+
(require '[progress.indeterminate :as pi])
6863

69-
```clojure
70-
(require '[progress.determinate :as pd] :reload-all)
64+
(print "Something uncountably slow is happening... ")
65+
(pi/animate! :opts {:frames (:clocks pi/styles)}
66+
(Thread/sleep 5000))
67+
(println)
7168

72-
(let [a (atom 0)]
73-
; Add up all the numbers from 1 to 100... ...slowly
74-
(pd/animate!
75-
a
76-
(reduce + (map #(do (Thread/sleep 10) (swap! a inc) %) (range 100)))))
77-
```
7869

79-
## Usage
70+
;; Determinate Task (aka "progress bar")
8071

81-
The functionality is provided by the `progress.indeterminate` and `progress.determinate` namespaces.
72+
(require '[progress.determinate :as pd])
8273

83-
Require them in the REPL:
84-
85-
```clojure
86-
(require '[progress.indeterminate :as pi] :reload-all)
87-
(require '[progress.determinate :as pd] :reload-all)
88-
```
89-
90-
Require them in your application:
91-
92-
```clojure
93-
(ns my-app.core
94-
(:require [progress.indeterminate :as pi]
95-
[progress.determinate :as pd]))
74+
(println "And now something countably slow is happening...")
75+
(let [a (atom 0)]
76+
(pd/animate! a :opts {:total 1000000
77+
:redraw-rate 60 ; Use 60 fps for the demo
78+
:style (:coloured-ascii-boxes pd/styles)} ; :emoji-boxes is also fun to try
79+
(run! (fn [_] (Thread/sleep 0 10) (swap! a inc)) (range 1000000)))) ; Count up to a million, slowly
80+
(println)
9681
```
9782

98-
### API Documentation
99-
100-
[API documentation is available here](https://pmonks.github.io/spinner/). The [unit](https://github.com/pmonks/spinner/blob/release/test/progress/indeterminate_test.clj) [tests](https://github.com/pmonks/spinner/blob/release/test/progress/determinate_test.clj) provide comprehensive usage examples (alternative animation sets, formatting, etc.).
101-
10283
## Contributor Information
10384

10485
[Contributing Guidelines](https://github.com/pmonks/spinner/blob/release/.github/CONTRIBUTING.md)

demo.clj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
;; Indeterminate Task (aka "spinner")
2+
3+
(require '[progress.indeterminate :as pi])
4+
5+
(print "Something uncountably slow is happening... ")
6+
(pi/animate! :opts {:frames (:clocks pi/styles)}
7+
(Thread/sleep 5000))
8+
(println)
9+
10+
11+
;; Determinate Task (aka "progress bar")
12+
13+
(require '[progress.determinate :as pd])
14+
15+
(println "And now something countably slow is happening...")
16+
(let [a (atom 0)]
17+
(pd/animate! a :opts {:total 1000000
18+
:redraw-rate 60 ; Use 60 fps for the demo
19+
:style (:coloured-ascii-boxes pd/styles)} ; :emoji-boxes is also fun to try
20+
(run! (fn [_] (Thread/sleep 0 10) (swap! a inc)) (range 1000000)))) ; Count up to a million, slowly
21+
(println)

spinner-demo.gif

-2.28 MB
Loading

spinner-demo.tape

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
Require clojure
22
Output spinner-demo.gif
33
Sleep 50ms
4-
Type "clojure -T:build test"
4+
Type "clojure -M demo.clj"
55
Sleep 250ms
66
Enter
77
Wait@90s
8+
Sleep 50ms

src/progress/ansi.clj

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,23 @@
2020
"Issues both SCO and DEC save-cursor ANSI codes, for maximum compatibility."
2121
[]
2222
(jansi/save-cursor!) ; JANSI uses SCO code for cursor positioning, which is unfortunate as they're less widely supported
23-
(print "\u001B7") ; So we manually send a DEC code too
24-
(flush))
23+
(print "\u001B7")) ; So we manually send a DEC code too
2524

2625
(defn restore-cursor!
2726
"Issues both SCO and DEC restore-cursor ANSI codes, for maximum compatibility."
2827
[]
2928
(jansi/restore-cursor!) ; JANSI uses SCO code for cursor positioning, which is unfortunate as they're less widely supported
30-
(print "\u001B8") ; So we manually send a DEC code too
31-
(flush))
29+
(print "\u001B8")) ; So we manually send a DEC code too
3230

3331
(defn hide-cursor!
32+
"Hides the cursor (not implemented by JANSI)."
3433
[]
35-
(print "\u001B[25l")
36-
(flush))
34+
(print "\u001B[25l"))
3735

3836
(defn show-cursor!
37+
"Shows the cursor (not implemented by JANSI)."
3938
[]
40-
(print "\u001B[25h")
41-
(flush))
39+
(print "\u001B[25h"))
4240

4341
(defn print-at
4442
"Send text output to the specified screen locations (note: ANSI screen

src/progress/determinate.clj

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,31 @@
3232
encoding, phase of the moon, and how long since your dog last pooped."
3333
{
3434
; ASCII determinate progress indicators are reliable across platforms
35-
:ascii-basic {:left "["
36-
:right "]"
37-
:empty " "
38-
:full "#"} ; Note: does not have a :tip
39-
:ascii-boxes {:left ""
40-
:right ""
41-
:empty " "
42-
:full ""
43-
:tip ""}
35+
:ascii-basic {:left "["
36+
:right "]"
37+
:empty " "
38+
:full "#"} ; Note: does not have a :tip
39+
:ascii-boxes {:left ""
40+
:right ""
41+
:empty " "
42+
:full ""
43+
:tip ""}
44+
:coloured-ascii-boxes {:empty ""
45+
:empty-fg-colour :bright-black
46+
:full ""
47+
:full-fg-colour :bright-white
48+
:tip ""
49+
:tip-fg-colour :bright-yellow}
4450

4551
; Emoji determinate progress indicators are unreliable across platforms (especially Windows)
46-
:emoji-circles {:left "" ; Note: double width without whitespace, despite appearances
47-
:right "" ; Note: double width without whitespace, despite appearances
48-
:empty ""
49-
:full "⚪️"
50-
:tip "🟡"}
51-
:emoji-boxes {:empty "⬛️"
52-
:full "⬜️"
53-
:tip "🟨"}})
52+
:emoji-circles {:left "" ; Note: double width without whitespace, despite appearances
53+
:right "" ; Note: double width without whitespace, despite appearances
54+
:empty ""
55+
:full "⚪️"
56+
:tip "🟡"}
57+
:emoji-boxes {:empty "⬛️"
58+
:full "⬜️"
59+
:tip "🟨"}})
5460

5561
(defn- clamp
5662
"Clamps a value within a range."
@@ -157,16 +163,15 @@
157163
(flush))))
158164

159165
(defn- poll-atom
160-
"Polls atom `value-atom` every `poll-interval-ms` and calls `render-fn!` (a
161-
function of one argument - the current value of the atom), if it has changed.
162-
Will stop when `running-promise?` is delivered a logically `false` value,
163-
returning `nil`."
164-
[value-atom running-promise? ^long poll-interval-ms render-fn!]
165-
(loop [previous-value nil]
166-
(let [current-value @value-atom]
166+
"Polls atom `a` every `poll-interval-ms` and calls `f` (a function of one
167+
argument - the current value of the atom), if it has changed. Will return
168+
`nil` when promise `p` is delivered a logically `false` value"
169+
[a p ^long poll-interval-ms f]
170+
(loop [previous-value ::undefined]
171+
(let [current-value @a]
167172
(when (not= current-value previous-value)
168-
(render-fn! current-value))
169-
(when (deref running-promise? poll-interval-ms true)
173+
(f current-value))
174+
(when (deref p poll-interval-ms true)
170175
(recur current-value))))
171176
nil)
172177

@@ -233,7 +238,7 @@
233238
empty-width (valid-width (:empty style))
234239
right-width (if-not (s/blank? (:right style)) (valid-width (:right style)) 0)
235240
digits-in-total (count (str total))
236-
counter-width (if counter? (+ 2 (* 2 digits-in-total)) 0) ; Include space delimier, / delimiter, current value and total
241+
counter-width (if counter? (+ 2 (* 2 digits-in-total)) 0) ; Include space delimiter, / delimiter, current value and total
237242
units-width (if (and counter? (not (s/blank? (:units style)))) (inc (valid-width (:units style))) 0) ; Include space delimiter
238243
body-width-cols (- width label-width left-width right-width counter-width units-width)
239244
unit-width-cols (max empty-width full-width tip-width)

src/progress/indeterminate.clj

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,15 @@
5656
(let [msg (s/join " " more)]
5757
(if (= @s :active)
5858
(swap! msgs str msg)
59-
(do
60-
(clojure.core/print msg) ; If a progress indicator isn't active, just print immediately
61-
(flush)))))
59+
(clojure.core/print msg)))) ; If a progress indicator isn't active, just print immediately
6260
nil)
6361

6462
(defn- print-pending-messages
6563
"Prints all pending messages"
6664
[]
6765
(when-let [messages (first (tp/swap*! msgs (constantly nil)))]
66+
(jansi/erase-line!)
6867
(clojure.core/print messages)
69-
(flush)
7068
(ansi/save-cursor!)))
7169

7270
(def default-style
@@ -125,20 +123,30 @@
125123
fg-colour :default
126124
bg-colour :default
127125
attributes [:default]}}]
128-
(let [delay-in-ms (long (Math/round (double delay-in-ms)))] ; Coerce delay-in-ms to a long
126+
(let [delay-in-ms (long (Math/round (double delay-in-ms)))] ; Coerce delay-in-ms to a long (especially if it's a Clojure ratio)
127+
; Setup logic
129128
(ansi/save-cursor!)
130129
(ansi/hide-cursor!)
130+
(jansi/erase-line!)
131+
(flush) ; Flush any outstanding I/O to stdout before we start animating
132+
; Main animation loop
131133
(loop [i 0]
132-
(clojure.core/print (str (ansi/apply-colours-and-attrs fg-colour bg-colour attributes (nth frames (mod i (count frames))))
134+
(clojure.core/print (str (ansi/apply-colours-and-attrs fg-colour bg-colour attributes (nth frames i))
133135
" "))
134-
(flush)
135-
(when (pos? delay-in-ms) (Thread/sleep delay-in-ms)) ; Thread/sleep throws on negative values, and sleeping for 0ms makes no sense
136-
(ansi/restore-cursor!)
137136
(ansi/show-cursor!)
138-
(jansi/erase-line!)
137+
(flush) ; Flush I/O to stdout at least once per loop
138+
(when (pos? delay-in-ms) (Thread/sleep delay-in-ms))
139+
(ansi/hide-cursor!)
140+
(ansi/restore-cursor!)
139141
(print-pending-messages)
140142
(when (active?)
141-
(recur (inc i)))))
143+
(recur (mod (inc i) (count frames))))))
144+
; Clean up logic
145+
(ansi/restore-cursor!)
146+
(jansi/erase-line!)
147+
(print-pending-messages)
148+
(ansi/show-cursor!)
149+
(flush) ; Flush any outstanding I/O to stdout
142150
nil))
143151

144152
(defn ^:no-doc start!
@@ -147,7 +155,6 @@
147155
([opts]
148156
(when-not (compare-and-set! s :inactive :active)
149157
(throw (java.lang.IllegalStateException. "Progress indicator is already active.")))
150-
(flush) ; Flush any residual I/O to stdout before we start animating
151158
(reset! msgs nil)
152159
(reset! fut (e/future* (indeterminate-progress-indicator opts)))
153160
nil))
@@ -157,8 +164,7 @@
157164
[]
158165
(when (compare-and-set! s :active :shutting-down)
159166
(try
160-
@@fut ; Wait for the future to stop (deref the atom AND the future)
161-
(print-pending-messages) ; Flush any remaining messages
167+
@@fut ; Wait for the future to stop (deref the atom AND the future)
162168
(finally
163169
(reset! fut nil)
164170
(reset! s :inactive))))

test/progress/indeterminate_test.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
(do
8484
(print (str "\n" (name style) ": "))
8585
(flush)
86-
(is (= nil (pi/animate! :opts {:frames (style pi/styles)} (Thread/sleep 250)))))))))
86+
(is (= nil (pi/animate! :opts {:frames (style pi/styles)} (Thread/sleep 500)))))))))
8787

8888
(testing "Printing messages while an animation is active"
8989
(is (= nil (do

0 commit comments

Comments
 (0)