Skip to content

Commit a5b6f2f

Browse files
committed
🎨 Optimise I/O flushing, and create dedicated demo script that's less ugly
1 parent 4b2241c commit a5b6f2f

File tree

8 files changed

+77
-74
lines changed

8 files changed

+77
-74
lines changed

README.md

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
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

@@ -25,6 +25,10 @@ Note that using Unicode characters in progress indicators may be unreliable, dep
2525

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

28+
## Usage
29+
30+
[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.).
31+
2832
### Trying it Out
2933

3034
**Important Notes:**
@@ -49,56 +53,32 @@ $ lein trampoline try com.github.pmonks/spinner
4953

5054
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).
5155

52-
#### Simple REPL Session
56+
### Demo
5357

54-
##### Indeterminate Task (aka "spinner")
58+
From `demo.clj`:
5559

5660
```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-
```
61+
;; Indeterminate Task (aka "spinner")
6662

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

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

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-
```
78-
79-
## Usage
8070

81-
The functionality is provided by the `progress.indeterminate` and `progress.determinate` namespaces.
71+
;; Determinate Task (aka "progress bar")
8272

83-
Require them in the REPL:
73+
(require '[progress.determinate :as pd])
8474

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]))
75+
(println "And now something countably slow is happening...")
76+
(let [a (atom 0)]
77+
(pd/animate! a :opts {:total 1000000 :redraw-rate 60 :style (:emoji-boxes pd/styles)}
78+
(run! (fn [_] (Thread/sleep 0 10) (swap! a inc)) (range 1000000)))) ; Count up to a million, slowly
79+
(println)
9680
```
9781

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-
10282
## Contributor Information
10383

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

demo.clj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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 :redraw-rate 60 :style (:emoji-boxes pd/styles)}
18+
(run! (fn [_] (Thread/sleep 0 10) (swap! a inc)) (range 1000000)))) ; Count up to a million, slowly
19+
(println)
20+
(flush)

spinner-demo.gif

-2.3 MB
Loading

spinner-demo.tape

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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

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: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -157,16 +157,15 @@
157157
(flush))))
158158

159159
(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]
160+
"Polls atom `a` every `poll-interval-ms` and calls `f` (a function of one
161+
argument - the current value of the atom), if it has changed. Will return
162+
`nil` when promise `p` is delivered a logically `false` value"
163+
[a p ^long poll-interval-ms f]
164+
(loop [previous-value ::undefined]
165+
(let [current-value @a]
167166
(when (not= current-value previous-value)
168-
(render-fn! current-value))
169-
(when (deref running-promise? poll-interval-ms true)
167+
(f current-value))
168+
(when (deref p poll-interval-ms true)
170169
(recur current-value))))
171170
nil)
172171

@@ -233,7 +232,7 @@
233232
empty-width (valid-width (:empty style))
234233
right-width (if-not (s/blank? (:right style)) (valid-width (:right style)) 0)
235234
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
235+
counter-width (if counter? (+ 2 (* 2 digits-in-total)) 0) ; Include space delimiter, / delimiter, current value and total
237236
units-width (if (and counter? (not (s/blank? (:units style)))) (inc (valid-width (:units style))) 0) ; Include space delimiter
238237
body-width-cols (- width label-width left-width right-width counter-width units-width)
239238
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)