;;; 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 ;; Version: 1.3.1 ;; 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 . ;;; 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.") (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 fps))) (frames (spinner--type-to-frames type)) (counter 0) (fps spinner-frames-per-second) (timer (timer-create) :read-only) (active-p nil) (buffer (when buffer-local (if (bufferp buffer-local) buffer-local (current-buffer))))) ;;;###autoload (defun spinner-create (&optional type buffer-local fps) "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." (make-spinner type buffer-local fps)) (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)) (elt (spinner--frames spinner) (spinner--counter spinner)))) (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) (% (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 at FPS frames per second." (let ((old-timer (spinner--timer spinner))) (when (timerp old-timer) (cancel-timer old-timer)) (setf (spinner--active-p spinner) t) ;; Create timer. (let* ((repeat (/ 1.0 (or (spinner--fps spinner) spinner-frames-per-second))) (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) "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'." (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))) (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)) (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))) (provide 'spinner) ;;; spinner.el ends here