summaryrefslogtreecommitdiff
path: root/spinner.el
blob: df59e1066ab26d7ab1eeafc38d3309deb63e6c04 (plain)
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
;;; spinner.el --- Add spinners and progress-bars to the mode-line for ongoing operations -*- lexical-binding: t; -*-

;; Copyright (C) 2015 Free Software Foundation, Inc.

;; Author: Artur Malabarba <emacs@endlessparentheses.com>
;; Version: 1.6
;; URL: https://github.com/Malabarba/spinner.el
;; Keywords: processes mode-line

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; 1 Usage
;; ═══════
;;
;;   First of all, don’t forget to add `(spinner "VERSION")' to your
;;   package’s dependencies.
;;
;;
;; 1.1 Major-modes
;; ───────────────
;;
;;   1. Just call `(spinner-start)' and a spinner will be added to the
;;      mode-line.
;;   2. Call `(spinner-stop)' on the same buffer when you want to remove
;;      it.
;;
;;   The default spinner is a line drawing that rotates. You can pass an
;;   argument to `spinner-start' to specify which spinner you want. All
;;   possibilities are listed in the `spinner-types' variable, but here are
;;   a few examples for you to try:
;;
;;   • `(spinner-start 'vertical-breathing 10)'
;;   • `(spinner-start 'minibox)'
;;   • `(spinner-start 'moon)'
;;   • `(spinner-start 'triangle)'
;;
;;   You can also define your own as a vector of strings (see the examples
;;   in `spinner-types').
;;
;;
;; 1.2 Minor-modes
;; ───────────────
;;
;;   Minor-modes can create a spinner with `spinner-create' and then add it
;;   to their mode-line lighter. They can then start the spinner by setting
;;   a variable and calling `spinner-start-timer'. Finally, they can stop
;;   the spinner (and the timer) by just setting the same variable to nil.
;;
;;   Here’s an example for a minor-mode named `foo'. Assuming that
;;   `foo--lighter' is used as the mode-line lighter, the following code
;;   will add an *inactive* global spinner to the mode-line.
;;   ┌────
;;   │ (defvar foo--spinner (spinner-create 'rotating-line))
;;   │ (defconst foo--lighter
;;   │   '(" foo" (:eval (spinner-print foo--spinner))))
;;   └────
;;
;;   1. To activate the spinner, just call `(spinner-start foo--spinner)'.
;;      It will show up on the mode-line and start animating.
;;   2. To get rid of it, call `(spinner-stop foo--spinner)'. It will then
;;      disappear again.
;;
;;   Some minor-modes will need spinners to be buffer-local. To achieve
;;   that, just make the `foo--spinner' variable buffer-local and use the
;;   third argument of the `spinner-create' function. The snippet below is an
;;   example.
;;
;;   ┌────
;;   │ (defvar-local foo--spinner nil)
;;   │ (defconst foo--lighter
;;   │   '(" foo" (:eval (spinner-print foo--spinner))))
;;   │ (defun foo--start-spinner ()
;;   │   "Create and start a spinner on this buffer."
;;   │   (unless foo--spinner
;;   │     (setq foo--spinner (spinner-create 'moon t)))
;;   │   (spinner-start foo--spinner))
;;   └────
;;
;;   1. To activate the spinner, just call `(foo--start-spinner)'.
;;   2. To get rid of it, call `(spinner-stop foo--spinner)'.
;;
;;   This will use the `moon' spinner, but you can use any of the names
;;   defined in the `spinner-types' variable or even define your own.


;;; Code:
(eval-when-compile
  (require 'cl))

(defconst spinner-types
  '((3-line-clock . ["┤" "┘" "┴" "└" "├" "┌" "┬" "┐"])
    (2-line-clock . ["┘" "└" "┌" "┐"])
    (flipping-line . ["_" "\\" "|" "/"])
    (rotating-line . ["-" "\\" "|" "/"])
    (progress-bar . ["[    ]" "[=   ]" "[==  ]" "[=== ]" "[====]" "[ ===]" "[  ==]" "[   =]"])
    (progress-bar-filled . ["|    |" "|█   |" "|██  |" "|███ |" "|████|" "| ███|" "|  ██|" "|   █|"])
    (vertical-breathing . ["▁" "▂" "▃" "▄" "▅" "▆" "▇" "█" "▇" "▆" "▅" "▄" "▃" "▂" "▁" " "])
    (vertical-rising . ["▁" "▄" "█" "▀" "▔"])
    (horizontal-breathing . [" " "▏" "▎" "▍" "▌" "▋" "▊" "▉" "▉" "▊" "▋" "▌" "▍" "▎" "▏"])
    (horizontal-breathing-long
     . ["  " "▎ " "▌ " "▊ " "█ " "█▎" "█▌" "█▊" "██" "█▊" "█▌" "█▎" "█ " "▊ " "▋ " "▌ " "▍ " "▎ " "▏ "])
    (horizontal-moving . ["  " "▌ " "█ " "▐▌" " █" " ▐"])
    (minibox . ["▖" "▘" "▝" "▗"])
    (triangle . ["◢" "◣" "◤" "◥"])
    (box-in-box . ["◰" "◳" "◲" "◱"])
    (box-in-circle . ["◴" "◷" "◶" "◵"])
    (half-circle . ["◐" "◓" "◑" "◒"])
    (moon . ["🌑" "🌘" "🌖" "🌕" "🌔" "🌒"]))
  "Predefined alist of spinners.
Each car is a symbol identifying the spinner, and each cdr is a
vector, the spinner itself.")

(defun spinner-make-progress-bar (width &optional char)
  "Return a vector of strings of the given WIDTH.
The vector is a valid spinner type and is similar to the
`progress-bar' spinner, except without the sorrounding brackets.
CHAR is the character to use for the moving bar (defaults to =)."
  (let ((whole-string (concat (make-string (1- width) ?\s)
                              (make-string 4 (or char ?=))
                              (make-string width ?\s))))
    (thread-last (mapcar (lambda (n) (substring whole-string n (+ n width)))
                         (number-sequence (+ width 3) 0 -1))
      (apply #'vector))))

(defvar spinner-current nil
  "Spinner curently being displayed on the `mode-line-process'.")
(make-variable-buffer-local 'spinner-current)

(defconst spinner--mode-line-construct
  '(:eval (spinner-print spinner-current))
  "Construct used to display a spinner in `mode-line-process'.")
(put 'spinner--mode-line-construct 'risky-local-variable t)

(defvar spinner-frames-per-second 10
  "Default speed at which spinners spin, in frames per second.
Each spinner can override this value.")


;;; The spinner object.
(defun spinner--type-to-frames (type)
  "Return a vector of frames corresponding to TYPE.
The list of possible built-in spinner types is given by the
`spinner-types' variable, but you can also use your own (see
below).

If TYPE is nil, the frames of this spinner are given by the first
element of `spinner-types'.
If TYPE is a symbol, it specifies an element of `spinner-types'.
If TYPE is 'random, use a random element of `spinner-types'.
If TYPE is a list, it should be a list of symbols, and a random
one is chosen as the spinner type.
If TYPE is a vector, it should be a vector of strings and these
are used as the spinner's frames.  This allows you to make your
own spinner animations."
  (cond
   ((vectorp type) type)
   ((not type) (cdr (car spinner-types)))
   ((eq type 'random)
    (cdr (elt spinner-types
              (random (length spinner-types)))))
   ((listp type)
    (cdr (assq (elt type (random (length type)))
               spinner-types)))
   ((symbolp type) (cdr (assq type spinner-types)))
   (t (error "Unknown spinner type: %s" type))))

(defstruct (spinner
            (:copier nil)
            (:conc-name spinner--)
            (:constructor make-spinner (&optional type buffer-local frames-per-second delay-before-start)))
  (frames (spinner--type-to-frames type))
  (counter 0)
  (fps (or frames-per-second spinner-frames-per-second))
  (timer (timer-create) :read-only)
  (active-p nil)
  (buffer (when buffer-local
            (if (bufferp buffer-local)
                buffer-local
              (current-buffer))))
  (delay (or delay-before-start 0)))

;;;###autoload
(defun spinner-create (&optional type buffer-local fps delay)
  "Create a spinner of the given TYPE.
The possible TYPEs are described in `spinner--type-to-frames'.

FPS, if given, is the number of desired frames per second.
Default is `spinner-frames-per-second'.

If BUFFER-LOCAL is non-nil, the spinner will be automatically
deactivated if the buffer is killed.  If BUFFER-LOCAL is a
buffer, use that instead of current buffer.

When started, in order to function properly, the spinner runs a
timer which periodically calls `force-mode-line-update' in the
curent buffer.  If BUFFER-LOCAL was set at creation time, then
`force-mode-line-update' is called in that buffer instead.  When
the spinner is stopped, the timer is deactivated.

DELAY, if given, is the number of seconds to wait after starting
the spinner before actually displaying it. It is safe to cancel
the spinner before this time, in which case it won't display at
all."
  (make-spinner type buffer-local fps delay))

(defun spinner-print (spinner)
  "Return a string of the current frame of SPINNER.
If SPINNER is nil, just return nil.
Designed to be used in the mode-line with:
    (:eval (spinner-print some-spinner))"
  (when (and spinner (spinner--active-p spinner))
    (let ((frame (spinner--counter spinner)))
      (when (>= frame 0)
        (elt (spinner--frames spinner) frame)))))

(defun spinner--timer-function (spinner)
  "Function called to update SPINNER.
If SPINNER is no longer active, or if its buffer has been killed,
stop the SPINNER's timer."
  (let ((buffer (spinner--buffer spinner)))
    (if (or (not (spinner--active-p spinner))
            (and buffer (not (buffer-live-p buffer))))
        (spinner-stop spinner)
      ;; Increment
      (callf (lambda (x) (if (< x 0)
                        (1+ x)
                      (% (1+ x) (length (spinner--frames spinner)))))
          (spinner--counter spinner))
      ;; Update mode-line.
      (if (buffer-live-p buffer)
          (with-current-buffer buffer
            (force-mode-line-update))
        (force-mode-line-update)))))

(defun spinner--start-timer (spinner)
  "Start a SPINNER's timer."
  (let ((old-timer (spinner--timer spinner)))
    (when (timerp old-timer)
      (cancel-timer old-timer))

    (setf (spinner--active-p spinner) t)

    (unless (ignore-errors (> (spinner--fps spinner) 0))
      (error "A spinner's FPS must be a positive number"))
    (setf (spinner--counter spinner) (- (* (or (spinner--delay spinner) 0)
                                    (spinner--fps spinner))))
    ;; Create timer.
    (let* ((repeat (/ 1.0 (spinner--fps spinner)))
           (time (timer-next-integral-multiple-of-time (current-time) repeat))
           ;; Create the timer as a lex variable so it can cancel itself.
           (timer (spinner--timer spinner)))
      (timer-set-time timer time repeat)
      (timer-set-function timer #'spinner--timer-function (list spinner))
      (timer-activate timer)
      ;; Return a stopping function.
      (lambda () (spinner-stop spinner)))))


;;; The main functions
;;;###autoload
(defun spinner-start (&optional type-or-object fps delay)
  "Start a mode-line spinner of given TYPE-OR-OBJECT.
If TYPE-OR-OBJECT is an object created with `make-spinner',
simply activate it.  This method is designed for minor modes, so
they can use the spinner as part of their lighter by doing:
    '(:eval (spinner-print THE-SPINNER))
To stop this spinner, call `spinner-stop' on it.

If TYPE-OR-OBJECT is anything else, a buffer-local spinner is
created with this type, and it is displayed in the
`mode-line-process' of the buffer it was created it.  Both
TYPE-OR-OBJECT and FPS are passed to `make-spinner' (which see).
To stop this spinner, call `spinner-stop' in the same buffer.

Either way, the return value is a function which can be called
anywhere to stop this spinner.  You can also call `spinner-stop'
in the same buffer where the spinner was created.

FPS, if given, is the number of desired frames per second.
Default is `spinner-frames-per-second'.

DELAY, if given, is the number of seconds to wait until actually
displaying the spinner. It is safe to cancel the spinner before
this time, in which case it won't display at all."
  (unless (spinner-p type-or-object)
    ;; Choose type.
    (if (spinner-p spinner-current)
        (setf (spinner--frames spinner-current) (spinner--type-to-frames type-or-object))
      (setq spinner-current (make-spinner type-or-object (current-buffer) fps delay)))
    (setq type-or-object spinner-current)
    ;; Maybe add to mode-line.
    (unless (memq 'spinner--mode-line-construct mode-line-process)
      (setq mode-line-process
            (list (or mode-line-process "")
                  'spinner--mode-line-construct))))

  ;; Create timer.
  (when fps (setf (spinner--fps type-or-object) fps))
  (when delay (setf (spinner--delay type-or-object) delay))
  (spinner--start-timer type-or-object))

(defun spinner-start-print (spinner)
  "Like `spinner-print', but also start SPINNER if it's not active."
  (unless (spinner--active-p spinner)
    (spinner-start spinner))
  (spinner-print spinner))

(defun spinner-stop (&optional spinner)
  "Stop the current buffer's spinner."
  (let* ((spinner (or spinner spinner-current))
         (timer (spinner--timer spinner)))
    (when (timerp timer)
      (cancel-timer timer))
    (setf (spinner--active-p spinner) nil)
    (force-mode-line-update)))

(provide 'spinner)

;;; spinner.el ends here