Skip to content

Latest commit

 

History

History
1794 lines (1697 loc) · 52.4 KB

spinners.org

File metadata and controls

1794 lines (1697 loc) · 52.4 KB

Terminal spinners for Elvish

Functions to print different types of progress spinners for use in Elvish scripts.

This file is written in literate programming style, to make it easy to explain. See spinners.elv for the generated file.

Table of Contents

Usage

Install the elvish-modules package using epm (you can put these statements in your rc.elv file as well, to automatically install the package upon startup if needed):

use epm
epm:install &silent-if-installed github.com/zzamboni/elvish-modules

In your rc.elv, load this module:

use github.com/zzamboni/elvish-modules/spinners
use github.com/zzamboni/elvish-modules/spinners

Available spinners

The spinner definitions are taken from the sindresorhus/cli-spinners project. You can view a demo of all the spinners with the spinners:demo command:

images/spinners-demo.gif

You can get the list of available spinners using the spinners:list function.

spinners:list | take 5
▶ arc
▶ arrow
▶ arrow2
▶ arrow3
▶ balloon

Showing spinner while a function runs

The easiest way to create a spinner is to use spinners:run to execute a lambda while displaying a spinner. The spinner will stop automatically when the lambda finishes:

spinners:run { sleep 3 }

By default, the spinner specified by $spinners:default-spinner will be used. You can change this by passing the &spinner option. You can also specify a title to show next to the spinner by passing the &title option:

spinners:run &spinner=arrow &title="Loading modules" { sleep 3 }

The input and output of spinners:run are connected to the lambda, so you can use it as part of a pipeline. E.g.:

spinners:run &title=(styled "Counting files" blue) &style=green { fd . . } | count
fd . . | spinners:run &title="Counting characters in filenames" { each [f]{ all $f } } | count

The following options are available:

  • &spinner - specify the spinner to use, by name. Default: $spinners:default-spinner.
  • &style - string or list of style transformers that will be applied to the spinner (only to the spinner itself). Default: empty list.
  • &title - string or styled object that will be shown after the spinner. Default: empty string.
  • &prefix - string or styled object that will be shown before the spinner. Default: empty string.
  • &indent - integer indicating an indentation level. The indent will appear before prefix if both are given. Default: 0
  • &cursor - boolean specifying whether the cursor should be shown while the spinner is running. Default: $false.
  • &persist - if and how the spinner should persist at the end. See Spinner persistence and exception handling below. Default: $false
  • &hide-exception - boolean specifying whether an exception generated by the lambda should be displayed as usual or hidden (useful with &persist=status). Default: $false.

A spinner definition consists of frames and an interval. If you don’t want to use one of the built-in spinners by specifying &spinner, you can use the following options to create a custom spinner:

  • &frames - a string or list containing the different steps for the spinner.
  • &interval - interval between the frames, in milliseconds.

If the lambda accepts an argument, the spinner ID will be passed to it. You can use it to manipulate the spinner while it runs using spinners:attr to modify its attributes and behavior (the same as the option names above). For example:

spinners:run &title="Starting title" &persist=$true [s]{
  sleep 3
  spinners:attr $s title "New title!"
  sleep 3
  spinners:attr $s spinner shark
  spinners:attr $s style [ red ]
  sleep 3
  spinners:attr $s persist success
}

Spinner persistence and exception handling

By default, the spinner disappears when it stops. Any exceptions thrown by the lambda are displayed as usual.

This behavior can be changed by the following options to spinners:run:

  • &hide-exception: if set, exceptions thrown by the lamda are caught and hidden. Most useful with &persist=status (see below).
  • &persist: if set, can have one of the following values:
    • success, error, warning, info are the default status symbols, defined in $spinners:persist-symbols:

      images/spinners-persist-symbols.jpg

    • status: if the lambda throws an exception sets error, otherwise success. Can be used with &hide-exception:

      images/spinners-persist-status.jpg

You can define custom persistence symbols by adding elements to $spinners:persist-symbols, indexed by the symbol name, and containing two keys symbol and color. For example:

spinners:persist-symbols[unicorn] = [ &symbol="🦄" &color=default ]
spinners:run &title="Getting a unicorn" &persist=unicorn { sleep 3 }

images/spinners-persist-unicorn.gif

Advanced use: manually starting and stopping spinners

If you want more flexibility in producing spinners, you can create, start and stop spinners by hand. The general sequence should be as follows:

  1. Create a spinner using the spinners:new function. This function returns the ID of the new spinner, which you must store to use in spinner operations. The spinners:new function accepts the same options as spinners:run to specify the parameters of the spinner. For example:
    s = (spinners:new &title="Test spinner" &persist=status &hide-exception)
        
  2. Start the spinner with the spinners:start function. This displays and starts the spinner running with a background function, so it returns immediately. Note: if you do this from the terminal, the spinner will display on top of the command line as you type other commands.
    spinners:start $s
        
  3. While the spinner runs, you can use the spinners:attr function to set any of its attributes (the same as the option names in the spinners:new function) to modify the spinner on the fly. You can even change the spinner type by changing the spinner attribute. You can also persist and continue the current spinner and continue in a new line with spinners:persist, and also persist the current one and continue with a new one with the spinners:persist-and-new function, which takes the same arguments as the spinners:run function.
    sleep 3
    spinners:attr $s title "New title!"
    sleep 2
    spinners:persist $s
    sleep 1
    spinners:attr $s spinner shark
    sleep 3
    spinners:attr $s status ?(fail error)
    spinners:persist-and-new $s &indent=2 &title=(styled "Next step" blue)
    sleep 3
        
  4. Stop the spinner with the spinners:stop function. You can use the &status option to pass an exception object or $ok which can be used to determine the final state of the spinner (e.g. if you created it with &persist=status):
    spinners:stop $s
        

Note: if you call spinners:start and spinners:stop by hand, be aware that when the spinner stops Elvish will produce a notification about the background process. To prevent this you have to set $notify-bg-job-success to $false before calling $spinners:stop. This is not necessary if you use spinners:run.

During the execution of a spinner, the spinners:persist function can be used to persist the current spinner (according to its settings), move the cursor to the next line and continue running it. The spinners:persist-and-new function can be used to persist the current spinner (according to its settings), move the cursor to the next line, and then continue with a new spinner. This function accepts the same arguments as spinners:run, settings from the old spinner are not inherited.

Implementation

Modules

use str
use path
use github.com/zzamboni/elvish-modules/tty

Initialization

When the module is loaded, we read the spinner definitions from spinners.json.

var spinners = (from-json < (path:dir (src)[name])/spinners.json)

By default the dots spinner is used.

var default-spinner = 'dots'

We store spinners in the $spinners:-sr variable, indexed by ID.

var -sr = [&]

Utility functions

All output is produced through this function.

fn -output {|@s|
  print $@s >/dev/tty
}

Spinner creation, advancing and status

A spinner object is a definition of a spinner, and is a map containing the following keys:

  • spinner - the name of one of the built-in spinners. This field implicitly defines frames and interval:
    • frames - a string or list containing the different steps for the spinner.
    • interval - interval between the frames, in milliseconds.
  • style - an optional list of style transformers that will be applied to the spinner characters.
  • title - an optional string or styled object that will be shown after the spinner.
  • prefix - an optional string or styled object that will be shown before the spinner.
  • indent - an optional integer indicating an indentation level. The indent will appear before prefix if both are given.
  • cursor - an optional boolean specifying whether the cursor should be shown while the spinner is running. Default is to hide it.
  • persist - an optional boolean specifying whether the spinner should be left in place and the cursor moved to the next line when the spinner finishes running. By default the spinner is cleared when it finishes running, and the cursor stays at the beginning of the line.
  • current - the current step of the spinner as it runs.
  • id - unique identifier for the spinner. By default generated as a UUID (if the uuidgen command is present) or a random number, but can be specified using the &id option if desired.

spinners:new creates a new spinner object containing the keys above, stores it in the registry, and returns its ID. The &spinner option can be used to initialize &frames and &interval from the default spinner definitions. If not given, $spinners:default-spinner is used. If &frames and &interval are given, they are used to override the default values. &title, &prefix and &style default to empty.

fn new {|&spinner=$nil &frames=$nil &interval=$nil &title="" &style=[] &prefix="" &indent=0 &cursor=$false &persist=$false &hide-exception=$false &id=$nil|
  # Determine ID to use
  set id = (or $id (var e = ?(uuidgen)) (randint 0 9999999))
  # Use default spinner if none is specified
  if (not $spinner) { set spinner = $default-spinner }
  # Automatically convert non-list styles, so you can do e.g. &style=red
  if (not-eq (kind-of $style) list) { set style = [$style] }
  # Create and store the new spinner object
  set -sr[$id] = [
    &id=             $id
    &spinner=        $spinner
    &frames=         (or $frames $spinners[$spinner][frames])
    &interval=       (or $interval $spinners[$spinner][interval])
    &title=          $title
    &prefix=         $prefix
    &indent=         $indent
    &style=          $style
    &cursor=         $cursor
    &persist=        $persist
    &hide-exception= $hide-exception
    &current=        0
    &status=         $ok
    &stop=           $false
  ]
  # Return ID of the new spinner
  put $id
}

Once a spinner object is created, spinners:step can be used to display and advance the spinner. This function returns an updated spinner object, which needs to be stored by the caller (Elvish does not support modifying arguments by reference).

fn step {|spinner|
  var steps = $-sr[$spinner][frames]
  var indentation = (str:join '' [(repeat $-sr[$spinner][indent] ' ')])
  var pre-string = (if (not-eq $-sr[$spinner][prefix] '') { put $-sr[$spinner][prefix]' ' } else { put '' })
  var post-string = (if (not-eq $-sr[$spinner][title] '') { put ' '$-sr[$spinner][title] } else { put '' })
  tty:set-cursor-pos (all $-sr[$spinner][initial-pos])
  -output $indentation$pre-string(styled $steps[$-sr[$spinner][current]] (all $-sr[$spinner][style]))$post-string
  tty:clear-line
  var inc = 1
  if (eq (kind-of $steps string)) {
    set inc = (count $steps[$-sr[$spinner][current]])
  }
  set -sr[$spinner][current] = (% (+ $-sr[$spinner][current] $inc) (count $steps))
}

Set the status of the spinner to different outcomes, will be displayed the next time the step function is called. The definition of the symbols to display can be customized by adding or changing elements in $spinners:persist-symbols.

var persist-symbols = [
  &success= [ &symbol="✔" &color=green ]
  &error=   [ &symbol="✖" &color=red ]
  &warning= [ &symbol="⚠" &color=yellow ]
  &info=    [ &symbol="ℹ" &color=blue ]
]
fn set-symbol {|spinner symbol|
  set -sr[$spinner][frames] = [ $persist-symbols[$symbol][symbol] ]
  set -sr[$spinner][style] = [ $persist-symbols[$symbol][color] ]
  set -sr[$spinner][current] = 0
}

Wait an amount of time as defined by the spinner’s interval field.

fn spinner-sleep {|s|
  sleep (to-string (/ $-sr[$s][interval] 1000))
}

Persist a spinner by setting its symbol to the one corresponding to its persist field (if any, otherwise just leave it as-is), and then moving the cursor to the next line and storing the new position as the spinner’s initial-pos field.

fn persist {|spinner|
  if (eq $-sr[$spinner][persist] status) {
    if $-sr[$spinner][status] {
      set-symbol $spinner success
    } else {
      set-symbol $spinner error
    }
  } elif (eq (kind-of $-sr[$spinner][persist]) string) {
    set-symbol $spinner $-sr[$spinner][persist]
  }
  step $spinner
  -output "\n"
  set -sr[$spinner][initial-pos] = [(tty:cursor-pos)]
}

Individual fields of a spinner can be queried or modified using the spinners:attr function. If no value is given, returns the value of the attribute, otherwise just sets it (no return value). The spinner field is treated specially by fetching the corresponding frames and interval attributes from the default spinner definitions, instead of just storing its value. It also resets the current counter to zero, since different spinners have different number of frames.

fn attr {|id attr @val|
  if (has-key $-sr $id) {
    if (eq $val []) {
      put $-sr[$id][$attr]
    } else {
      if (eq $attr spinner) {
        # Automatically populate frames and interval based on spinner
        var name = $val[0]
        set -sr[$id][spinner]  = $name
        set -sr[$id][frames]   = $spinners[$name][frames]
        set -sr[$id][interval] = $spinners[$name][interval]
        set -sr[$id][current]  = 0
      } elif (eq $attr style) {
        # Automatically convert non-list styles, so you can do e.g. &style=red
        var style = $val[0]
        if (not-eq (kind-of $style) list) { set style = [$style] }
        set -sr[$id][style] = $style
      } else {
        set -sr[$id][$attr] = $val[0]
      }
    }
  } else {
    fail "Nonexisting spinner with ID "$id
  }
}

Starting and stopping a spinner

Start a and stop a background spinner. The spinner is shown by a background function which will run until the spinner’s stop flag is set.

Note: if you call spinners:start and spinners:stop by hand, be aware that when the spinner stops Elvish will produce a notification about the background process. To prevent this you have to set $notify-bg-job-success to $false before calling $spinners:stop. This is not necessary if you use spinners:run.

The spinners:do-spinner function is the one that actually does the work of:

  • Hiding the cursor if necessary;
  • Cycling the spinner until its stop field is set (this requires a parallel process that sets this flag eventually);
  • Persisting the spinner with the appropriate symbol or clearing it, according to its configuration;
  • Reissuing any exceptions, if necessary;
  • Deleting the spinner definition from the internal registry.
fn do-spinner {|spinner|
  if (not $-sr[$spinner][cursor]) {
    tty:hide-cursor
  }
  set -sr[$spinner][initial-pos] = [(tty:cursor-pos)]
  while (not $-sr[$spinner][stop]) {
    step $spinner
    spinner-sleep $spinner
    if (has-key $-sr[$spinner] next-spinner-id) {
      var next-spinner-id = $-sr[$spinner][next-spinner-id]
      # Indicator to persist the current spinner and continue with a new definition
      persist $spinner
      set -sr[$spinner] = $-sr[$next-spinner-id]
      set -sr[$spinner][id] = $spinner
      set -sr[$spinner][initial-pos] = [(tty:cursor-pos)]
      del -sr[$next-spinner-id]
    }
  }
  if $-sr[$spinner][persist] {
    persist $spinner
  } else {
    tty:set-cursor-pos (all $-sr[$spinner][initial-pos])
    tty:clear-line
  }
  if (not $-sr[$spinner][cursor]) { tty:show-cursor }
  if (and (not $-sr[$spinner][status]) (not $-sr[$spinner][hide-exception])) {
    show $-sr[$spinner][status]
  }
  del -sr[$spinner]
}

The spinners:start function simply calls do-spinner in the background.

fn start {|spinner|
  do-spinner $spinner &
}

Stop execution of a spinner by setting its stop flag. This will be caught by the spinner process in the background, which does the work of stopping, persisting and removing the spinner. The &status option should be used to pass an exception object used to set the status of the spinner.

fn stop {|spinner &status=$ok|
  set -sr[$spinner][status] = $status
  set -sr[$spinner][stop] = $true
}

Running a function with a spinner

Simplest point of entry for displaying a spinner while a function is running. Takes a lambda as the only mandatory argument. A spinner will be automatically created and displayed until the lambda finishes. It takes the same options as spinners:new, which can be used to specify the details of the spinner to use.

fn run {|&spinner=$nil &frames=$nil &interval=$nil &title="" &style=[] &prefix="" &indent=0 &cursor=$false &persist=$false &hide-exception=$false f|
  # Create spinner
  var s = (new &spinner=$spinner &frames=$frames &interval=$interval &title=$title &style=$style &prefix=$prefix &indent=$indent &cursor=$cursor &persist=$persist &hide-exception=$hide-exception)
  # Determine whether to pass the spinner ID to the function
  var f-args = [$s]
  if (eq $f[arg-names] []) { set f-args = [] }
  # Run spinner in parallel with the function
  var status = $ok
  run-parallel {
    do-spinner $s
  } {
    set status = $ok
    try {
      $f $@f-args
    } catch e {
      set status = $e
    } finally {
      # Short pause to avoid a potential race condition when the
      # function finishes too quickly
      sleep 0.05
      stop &status=$status $s
    }
  }
}

During the execution of a spinner, the spinners:persist-and-new function can be used to persist the current spinner (according to its settings), move the cursor to the next line, and then continue with a new spinner. This function accepts the same arguments as spinners:run, settings from the old spinner are not inherited.

fn persist-and-new {|old-spinner &spinner=$nil &frames=$nil &interval=$nil &title="" &style=[] &prefix="" &indent=0 &cursor=$false &persist=$false &hide-exception=$false|
  var new-spinner = (new &spinner=$spinner &frames=$frames &interval=$interval &title=$title &style=$style &prefix=$prefix &indent=$indent &cursor=$cursor &persist=$persist &hide-exception=$hide-exception)
  set -sr[$old-spinner][next-spinner-id] = $new-spinner
}

List and demo mode

Return the list of available spinners

fn list {
  keys $spinners | order
}

Produce all the spinners in sequence.

fn demo {|&time=2 &style=blue &persist=$false|
  list | each {|s|
    run &spinner=$s &title=$s &style=$style &persist=$persist { sleep $time }
  }
}

Spinner definitions

The spinner definitions are taken from the sindresorhus/cli-spinners project, released under the following license:

MIT License

Copyright (c) Sindre Sorhus <[email protected]> (https://sindresorhus.com)

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
{
    "dots": {
        "interval": 80,
        "frames": [
            "⠋",
            "⠙",
            "⠹",
            "⠸",
            "⠼",
            "⠴",
            "⠦",
            "⠧",
            "⠇",
            "⠏"
        ]
    },
    "dots2": {
        "interval": 80,
        "frames": [
            "⣾",
            "⣽",
            "⣻",
            "⢿",
            "⡿",
            "⣟",
            "⣯",
            "⣷"
        ]
    },
    "dots3": {
        "interval": 80,
        "frames": [
            "⠋",
            "⠙",
            "⠚",
            "⠞",
            "⠖",
            "⠦",
            "⠴",
            "⠲",
            "⠳",
            "⠓"
        ]
    },
    "dots4": {
        "interval": 80,
        "frames": [
            "⠄",
            "⠆",
            "⠇",
            "⠋",
            "⠙",
            "⠸",
            "⠰",
            "⠠",
            "⠰",
            "⠸",
            "⠙",
            "⠋",
            "⠇",
            "⠆"
        ]
    },
    "dots5": {
        "interval": 80,
        "frames": [
            "⠋",
            "⠙",
            "⠚",
            "⠒",
            "⠂",
            "⠂",
            "⠒",
            "⠲",
            "⠴",
            "⠦",
            "⠖",
            "⠒",
            "⠐",
            "⠐",
            "⠒",
            "⠓",
            "⠋"
        ]
    },
    "dots6": {
        "interval": 80,
        "frames": [
            "⠁",
            "⠉",
            "⠙",
            "⠚",
            "⠒",
            "⠂",
            "⠂",
            "⠒",
            "⠲",
            "⠴",
            "⠤",
            "⠄",
            "⠄",
            "⠤",
            "⠴",
            "⠲",
            "⠒",
            "⠂",
            "⠂",
            "⠒",
            "⠚",
            "⠙",
            "⠉",
            "⠁"
        ]
    },
    "dots7": {
        "interval": 80,
        "frames": [
            "⠈",
            "⠉",
            "⠋",
            "⠓",
            "⠒",
            "⠐",
            "⠐",
            "⠒",
            "⠖",
            "⠦",
            "⠤",
            "⠠",
            "⠠",
            "⠤",
            "⠦",
            "⠖",
            "⠒",
            "⠐",
            "⠐",
            "⠒",
            "⠓",
            "⠋",
            "⠉",
            "⠈"
        ]
    },
    "dots8": {
        "interval": 80,
        "frames": [
            "⠁",
            "⠁",
            "⠉",
            "⠙",
            "⠚",
            "⠒",
            "⠂",
            "⠂",
            "⠒",
            "⠲",
            "⠴",
            "⠤",
            "⠄",
            "⠄",
            "⠤",
            "⠠",
            "⠠",
            "⠤",
            "⠦",
            "⠖",
            "⠒",
            "⠐",
            "⠐",
            "⠒",
            "⠓",
            "⠋",
            "⠉",
            "⠈",
            "⠈"
        ]
    },
    "dots9": {
        "interval": 80,
        "frames": [
            "⢹",
            "⢺",
            "⢼",
            "⣸",
            "⣇",
            "⡧",
            "⡗",
            "⡏"
        ]
    },
    "dots10": {
        "interval": 80,
        "frames": [
            "⢄",
            "⢂",
            "⢁",
            "⡁",
            "⡈",
            "⡐",
            "⡠"
        ]
    },
    "dots11": {
        "interval": 100,
        "frames": [
            "⠁",
            "⠂",
            "⠄",
            "⡀",
            "⢀",
            "⠠",
            "⠐",
            "⠈"
        ]
    },
    "dots12": {
        "interval": 80,
        "frames": [
            "⢀⠀",
            "⡀⠀",
            "⠄⠀",
            "⢂⠀",
            "⡂⠀",
            "⠅⠀",
            "⢃⠀",
            "⡃⠀",
            "⠍⠀",
            "⢋⠀",
            "⡋⠀",
            "⠍⠁",
            "⢋⠁",
            "⡋⠁",
            "⠍⠉",
            "⠋⠉",
            "⠋⠉",
            "⠉⠙",
            "⠉⠙",
            "⠉⠩",
            "⠈⢙",
            "⠈⡙",
            "⢈⠩",
            "⡀⢙",
            "⠄⡙",
            "⢂⠩",
            "⡂⢘",
            "⠅⡘",
            "⢃⠨",
            "⡃⢐",
            "⠍⡐",
            "⢋⠠",
            "⡋⢀",
            "⠍⡁",
            "⢋⠁",
            "⡋⠁",
            "⠍⠉",
            "⠋⠉",
            "⠋⠉",
            "⠉⠙",
            "⠉⠙",
            "⠉⠩",
            "⠈⢙",
            "⠈⡙",
            "⠈⠩",
            "⠀⢙",
            "⠀⡙",
            "⠀⠩",
            "⠀⢘",
            "⠀⡘",
            "⠀⠨",
            "⠀⢐",
            "⠀⡐",
            "⠀⠠",
            "⠀⢀",
            "⠀⡀"
        ]
    },
    "dots8Bit": {
        "interval": 80,
        "frames": [
            "⠀",
            "⠁",
            "⠂",
            "⠃",
            "⠄",
            "⠅",
            "⠆",
            "⠇",
            "⡀",
            "⡁",
            "⡂",
            "⡃",
            "⡄",
            "⡅",
            "⡆",
            "⡇",
            "⠈",
            "⠉",
            "⠊",
            "⠋",
            "⠌",
            "⠍",
            "⠎",
            "⠏",
            "⡈",
            "⡉",
            "⡊",
            "⡋",
            "⡌",
            "⡍",
            "⡎",
            "⡏",
            "⠐",
            "⠑",
            "⠒",
            "⠓",
            "⠔",
            "⠕",
            "⠖",
            "⠗",
            "⡐",
            "⡑",
            "⡒",
            "⡓",
            "⡔",
            "⡕",
            "⡖",
            "⡗",
            "⠘",
            "⠙",
            "⠚",
            "⠛",
            "⠜",
            "⠝",
            "⠞",
            "⠟",
            "⡘",
            "⡙",
            "⡚",
            "⡛",
            "⡜",
            "⡝",
            "⡞",
            "⡟",
            "⠠",
            "⠡",
            "⠢",
            "⠣",
            "⠤",
            "⠥",
            "⠦",
            "⠧",
            "⡠",
            "⡡",
            "⡢",
            "⡣",
            "⡤",
            "⡥",
            "⡦",
            "⡧",
            "⠨",
            "⠩",
            "⠪",
            "⠫",
            "⠬",
            "⠭",
            "⠮",
            "⠯",
            "⡨",
            "⡩",
            "⡪",
            "⡫",
            "⡬",
            "⡭",
            "⡮",
            "⡯",
            "⠰",
            "⠱",
            "⠲",
            "⠳",
            "⠴",
            "⠵",
            "⠶",
            "⠷",
            "⡰",
            "⡱",
            "⡲",
            "⡳",
            "⡴",
            "⡵",
            "⡶",
            "⡷",
            "⠸",
            "⠹",
            "⠺",
            "⠻",
            "⠼",
            "⠽",
            "⠾",
            "⠿",
            "⡸",
            "⡹",
            "⡺",
            "⡻",
            "⡼",
            "⡽",
            "⡾",
            "⡿",
            "⢀",
            "⢁",
            "⢂",
            "⢃",
            "⢄",
            "⢅",
            "⢆",
            "⢇",
            "⣀",
            "⣁",
            "⣂",
            "⣃",
            "⣄",
            "⣅",
            "⣆",
            "⣇",
            "⢈",
            "⢉",
            "⢊",
            "⢋",
            "⢌",
            "⢍",
            "⢎",
            "⢏",
            "⣈",
            "⣉",
            "⣊",
            "⣋",
            "⣌",
            "⣍",
            "⣎",
            "⣏",
            "⢐",
            "⢑",
            "⢒",
            "⢓",
            "⢔",
            "⢕",
            "⢖",
            "⢗",
            "⣐",
            "⣑",
            "⣒",
            "⣓",
            "⣔",
            "⣕",
            "⣖",
            "⣗",
            "⢘",
            "⢙",
            "⢚",
            "⢛",
            "⢜",
            "⢝",
            "⢞",
            "⢟",
            "⣘",
            "⣙",
            "⣚",
            "⣛",
            "⣜",
            "⣝",
            "⣞",
            "⣟",
            "⢠",
            "⢡",
            "⢢",
            "⢣",
            "⢤",
            "⢥",
            "⢦",
            "⢧",
            "⣠",
            "⣡",
            "⣢",
            "⣣",
            "⣤",
            "⣥",
            "⣦",
            "⣧",
            "⢨",
            "⢩",
            "⢪",
            "⢫",
            "⢬",
            "⢭",
            "⢮",
            "⢯",
            "⣨",
            "⣩",
            "⣪",
            "⣫",
            "⣬",
            "⣭",
            "⣮",
            "⣯",
            "⢰",
            "⢱",
            "⢲",
            "⢳",
            "⢴",
            "⢵",
            "⢶",
            "⢷",
            "⣰",
            "⣱",
            "⣲",
            "⣳",
            "⣴",
            "⣵",
            "⣶",
            "⣷",
            "⢸",
            "⢹",
            "⢺",
            "⢻",
            "⢼",
            "⢽",
            "⢾",
            "⢿",
            "⣸",
            "⣹",
            "⣺",
            "⣻",
            "⣼",
            "⣽",
            "⣾",
            "⣿"
        ]
    },
    "line": {
        "interval": 130,
        "frames": [
            "-",
            "\\",
            "|",
            "/"
        ]
    },
    "line2": {
        "interval": 100,
        "frames": [
            "⠂",
            "-",
            "–",
            "—",
            "–",
            "-"
        ]
    },
    "pipe": {
        "interval": 100,
        "frames": [
            "┤",
            "┘",
            "┴",
            "└",
            "├",
            "┌",
            "┬",
            "┐"
        ]
    },
    "simpleDots": {
        "interval": 400,
        "frames": [
            ".  ",
            ".. ",
            "...",
            "   "
        ]
    },
    "simpleDotsScrolling": {
        "interval": 200,
        "frames": [
            ".  ",
            ".. ",
            "...",
            " ..",
            "  .",
            "   "
        ]
    },
    "star": {
        "interval": 70,
        "frames": [
            "✶",
            "✸",
            "✹",
            "✺",
            "✹",
            "✷"
        ]
    },
    "star2": {
        "interval": 80,
        "frames": [
            "+",
            "x",
            "*"
        ]
    },
    "flip": {
        "interval": 70,
        "frames": [
            "_",
            "_",
            "_",
            "-",
            "`",
            "`",
            "'",
            "´",
            "-",
            "_",
            "_",
            "_"
        ]
    },
    "hamburger": {
        "interval": 100,
        "frames": [
            "☱",
            "☲",
            "☴"
        ]
    },
    "growVertical": {
        "interval": 120,
        "frames": [
            "▁",
            "▃",
            "▄",
            "▅",
            "▆",
            "▇",
            "▆",
            "▅",
            "▄",
            "▃"
        ]
    },
    "growHorizontal": {
        "interval": 120,
        "frames": [
            "▏",
            "▎",
            "▍",
            "▌",
            "▋",
            "▊",
            "▉",
            "▊",
            "▋",
            "▌",
            "▍",
            "▎"
        ]
    },
    "balloon": {
        "interval": 140,
        "frames": [
            " ",
            ".",
            "o",
            "O",
            "@",
            "*",
            " "
        ]
    },
    "balloon2": {
        "interval": 120,
        "frames": [
            ".",
            "o",
            "O",
            "°",
            "O",
            "o",
            "."
        ]
    },
    "noise": {
        "interval": 100,
        "frames": [
            "▓",
            "▒",
            "░"
        ]
    },
    "bounce": {
        "interval": 120,
        "frames": [
            "⠁",
            "⠂",
            "⠄",
            "⠂"
        ]
    },
    "boxBounce": {
        "interval": 120,
        "frames": [
            "▖",
            "▘",
            "▝",
            "▗"
        ]
    },
    "boxBounce2": {
        "interval": 100,
        "frames": [
            "▌",
            "▀",
            "▐",
            "▄"
        ]
    },
    "triangle": {
        "interval": 50,
        "frames": [
            "◢",
            "◣",
            "◤",
            "◥"
        ]
    },
    "arc": {
        "interval": 100,
        "frames": [
            "◜",
            "◠",
            "◝",
            "◞",
            "◡",
            "◟"
        ]
    },
    "circle": {
        "interval": 120,
        "frames": [
            "◡",
            "⊙",
            "◠"
        ]
    },
    "squareCorners": {
        "interval": 180,
        "frames": [
            "◰",
            "◳",
            "◲",
            "◱"
        ]
    },
    "circleQuarters": {
        "interval": 120,
        "frames": [
            "◴",
            "◷",
            "◶",
            "◵"
        ]
    },
    "circleHalves": {
        "interval": 50,
        "frames": [
            "◐",
            "◓",
            "◑",
            "◒"
        ]
    },
    "squish": {
        "interval": 100,
        "frames": [
            "╫",
            "╪"
        ]
    },
    "toggle": {
        "interval": 250,
        "frames": [
            "⊶",
            "⊷"
        ]
    },
    "toggle2": {
        "interval": 80,
        "frames": [
            "▫",
            "▪"
        ]
    },
    "toggle3": {
        "interval": 120,
        "frames": [
            "□",
            "■"
        ]
    },
    "toggle4": {
        "interval": 100,
        "frames": [
            "■",
            "□",
            "▪",
            "▫"
        ]
    },
    "toggle5": {
        "interval": 100,
        "frames": [
            "▮",
            "▯"
        ]
    },
    "toggle6": {
        "interval": 300,
        "frames": [
            "ဝ",
            "၀"
        ]
    },
    "toggle7": {
        "interval": 80,
        "frames": [
            "⦾",
            "⦿"
        ]
    },
    "toggle8": {
        "interval": 100,
        "frames": [
            "◍",
            "◌"
        ]
    },
    "toggle9": {
        "interval": 100,
        "frames": [
            "◉",
            "◎"
        ]
    },
    "toggle10": {
        "interval": 100,
        "frames": [
            "㊂",
            "㊀",
            "㊁"
        ]
    },
    "toggle11": {
        "interval": 50,
        "frames": [
            "⧇",
            "⧆"
        ]
    },
    "toggle12": {
        "interval": 120,
        "frames": [
            "☗",
            "☖"
        ]
    },
    "toggle13": {
        "interval": 80,
        "frames": [
            "=",
            "*",
            "-"
        ]
    },
    "arrow": {
        "interval": 100,
        "frames": [
            "←",
            "↖",
            "↑",
            "↗",
            "→",
            "↘",
            "↓",
            "↙"
        ]
    },
    "arrow2": {
        "interval": 80,
        "frames": [
            "⬆️ ",
            "↗️ ",
            "➡️ ",
            "↘️ ",
            "⬇️ ",
            "↙️ ",
            "⬅️ ",
            "↖️ "
        ]
    },
    "arrow3": {
        "interval": 120,
        "frames": [
            "▹▹▹▹▹",
            "▸▹▹▹▹",
            "▹▸▹▹▹",
            "▹▹▸▹▹",
            "▹▹▹▸▹",
            "▹▹▹▹▸"
        ]
    },
    "bouncingBar": {
        "interval": 80,
        "frames": [
            "[    ]",
            "[=   ]",
            "[==  ]",
            "[=== ]",
            "[ ===]",
            "[  ==]",
            "[   =]",
            "[    ]",
            "[   =]",
            "[  ==]",
            "[ ===]",
            "[====]",
            "[=== ]",
            "[==  ]",
            "[=   ]"
        ]
    },
    "bouncingBall": {
        "interval": 80,
        "frames": [
            "( ●    )",
            "(  ●   )",
            "(   ●  )",
            "(    ● )",
            "(     ●)",
            "(    ● )",
            "(   ●  )",
            "(  ●   )",
            "( ●    )",
            "(●     )"
        ]
    },
    "smiley": {
        "interval": 200,
        "frames": [
            "😄 ",
            "😝 "
        ]
    },
    "monkey": {
        "interval": 300,
        "frames": [
            "🙈 ",
            "🙈 ",
            "🙉 ",
            "🙊 "
        ]
    },
    "hearts": {
        "interval": 100,
        "frames": [
            "💛 ",
            "💙 ",
            "💜 ",
            "💚 ",
            "❤️ "
        ]
    },
    "clock": {
        "interval": 100,
        "frames": [
            "🕛 ",
            "🕐 ",
            "🕑 ",
            "🕒 ",
            "🕓 ",
            "🕔 ",
            "🕕 ",
            "🕖 ",
            "🕗 ",
            "🕘 ",
            "🕙 ",
            "🕚 "
        ]
    },
    "earth": {
        "interval": 180,
        "frames": [
            "🌍 ",
            "🌎 ",
            "🌏 "
        ]
    },
    "material": {
        "interval": 17,
        "frames": [
            "█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "███████▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "████████▁▁▁▁▁▁▁▁▁▁▁▁",
            "█████████▁▁▁▁▁▁▁▁▁▁▁",
            "█████████▁▁▁▁▁▁▁▁▁▁▁",
            "██████████▁▁▁▁▁▁▁▁▁▁",
            "███████████▁▁▁▁▁▁▁▁▁",
            "█████████████▁▁▁▁▁▁▁",
            "██████████████▁▁▁▁▁▁",
            "██████████████▁▁▁▁▁▁",
            "▁██████████████▁▁▁▁▁",
            "▁██████████████▁▁▁▁▁",
            "▁██████████████▁▁▁▁▁",
            "▁▁██████████████▁▁▁▁",
            "▁▁▁██████████████▁▁▁",
            "▁▁▁▁█████████████▁▁▁",
            "▁▁▁▁██████████████▁▁",
            "▁▁▁▁██████████████▁▁",
            "▁▁▁▁▁██████████████▁",
            "▁▁▁▁▁██████████████▁",
            "▁▁▁▁▁██████████████▁",
            "▁▁▁▁▁▁██████████████",
            "▁▁▁▁▁▁██████████████",
            "▁▁▁▁▁▁▁█████████████",
            "▁▁▁▁▁▁▁█████████████",
            "▁▁▁▁▁▁▁▁████████████",
            "▁▁▁▁▁▁▁▁████████████",
            "▁▁▁▁▁▁▁▁▁███████████",
            "▁▁▁▁▁▁▁▁▁███████████",
            "▁▁▁▁▁▁▁▁▁▁██████████",
            "▁▁▁▁▁▁▁▁▁▁██████████",
            "▁▁▁▁▁▁▁▁▁▁▁▁████████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
            "█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
            "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
            "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
            "███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
            "████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
            "█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
            "█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
            "██████▁▁▁▁▁▁▁▁▁▁▁▁▁█",
            "████████▁▁▁▁▁▁▁▁▁▁▁▁",
            "█████████▁▁▁▁▁▁▁▁▁▁▁",
            "█████████▁▁▁▁▁▁▁▁▁▁▁",
            "█████████▁▁▁▁▁▁▁▁▁▁▁",
            "█████████▁▁▁▁▁▁▁▁▁▁▁",
            "███████████▁▁▁▁▁▁▁▁▁",
            "████████████▁▁▁▁▁▁▁▁",
            "████████████▁▁▁▁▁▁▁▁",
            "██████████████▁▁▁▁▁▁",
            "██████████████▁▁▁▁▁▁",
            "▁██████████████▁▁▁▁▁",
            "▁██████████████▁▁▁▁▁",
            "▁▁▁█████████████▁▁▁▁",
            "▁▁▁▁▁████████████▁▁▁",
            "▁▁▁▁▁████████████▁▁▁",
            "▁▁▁▁▁▁███████████▁▁▁",
            "▁▁▁▁▁▁▁▁█████████▁▁▁",
            "▁▁▁▁▁▁▁▁█████████▁▁▁",
            "▁▁▁▁▁▁▁▁▁█████████▁▁",
            "▁▁▁▁▁▁▁▁▁█████████▁▁",
            "▁▁▁▁▁▁▁▁▁▁█████████▁",
            "▁▁▁▁▁▁▁▁▁▁▁████████▁",
            "▁▁▁▁▁▁▁▁▁▁▁████████▁",
            "▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
            "▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
            "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁"
        ]
    },
    "moon": {
        "interval": 80,
        "frames": [
            "🌑 ",
            "🌒 ",
            "🌓 ",
            "🌔 ",
            "🌕 ",
            "🌖 ",
            "🌗 ",
            "🌘 "
        ]
    },
    "runner": {
        "interval": 140,
        "frames": [
            "🚶 ",
            "🏃 "
        ]
    },
    "pong": {
        "interval": 80,
        "frames": [
            "▐⠂       ▌",
            "▐⠈       ▌",
            "▐ ⠂      ▌",
            "▐ ⠠      ▌",
            "▐  ⡀     ▌",
            "▐  ⠠     ▌",
            "▐   ⠂    ▌",
            "▐   ⠈    ▌",
            "▐    ⠂   ▌",
            "▐    ⠠   ▌",
            "▐     ⡀  ▌",
            "▐     ⠠  ▌",
            "▐      ⠂ ▌",
            "▐      ⠈ ▌",
            "▐       ⠂▌",
            "▐       ⠠▌",
            "▐       ⡀▌",
            "▐      ⠠ ▌",
            "▐      ⠂ ▌",
            "▐     ⠈  ▌",
            "▐     ⠂  ▌",
            "▐    ⠠   ▌",
            "▐    ⡀   ▌",
            "▐   ⠠    ▌",
            "▐   ⠂    ▌",
            "▐  ⠈     ▌",
            "▐  ⠂     ▌",
            "▐ ⠠      ▌",
            "▐ ⡀      ▌",
            "▐⠠       ▌"
        ]
    },
    "shark": {
        "interval": 120,
        "frames": [
            "▐|\\____________▌",
            "▐_|\\___________▌",
            "▐__|\\__________▌",
            "▐___|\\_________▌",
            "▐____|\\________▌",
            "▐_____|\\_______▌",
            "▐______|\\______▌",
            "▐_______|\\_____▌",
            "▐________|\\____▌",
            "▐_________|\\___▌",
            "▐__________|\\__▌",
            "▐___________|\\_▌",
            "▐____________|\\▌",
            "▐____________/|▌",
            "▐___________/|_▌",
            "▐__________/|__▌",
            "▐_________/|___▌",
            "▐________/|____▌",
            "▐_______/|_____▌",
            "▐______/|______▌",
            "▐_____/|_______▌",
            "▐____/|________▌",
            "▐___/|_________▌",
            "▐__/|__________▌",
            "▐_/|___________▌",
            "▐/|____________▌"
        ]
    },
    "dqpb": {
        "interval": 100,
        "frames": [
            "d",
            "q",
            "p",
            "b"
        ]
    },
    "weather": {
        "interval": 100,
        "frames": [
            "☀️ ",
            "☀️ ",
            "☀️ ",
            "🌤 ",
            "⛅️ ",
            "🌥 ",
            "☁️ ",
            "🌧 ",
            "🌨 ",
            "🌧 ",
            "🌨 ",
            "🌧 ",
            "🌨 ",
            "⛈ ",
            "🌨 ",
            "🌧 ",
            "🌨 ",
            "☁️ ",
            "🌥 ",
            "⛅️ ",
            "🌤 ",
            "☀️ ",
            "☀️ "
        ]
    },
    "christmas": {
        "interval": 400,
        "frames": [
            "🌲",
            "🎄"
        ]
    },
    "grenade": {
        "interval": 80,
        "frames": [
            "،   ",
            "′   ",
            " ´ ",
            " ‾ ",
            "  ⸌",
            "  ⸊",
            "  |",
            "  ⁎",
            "  ⁕",
            " ෴ ",
            "  ⁓",
            "   ",
            "   ",
            "   "
        ]
    },
    "point": {
        "interval": 125,
        "frames": [
            "∙∙∙",
            "●∙∙",
            "∙●∙",
            "∙∙●",
            "∙∙∙"
        ]
    },
    "layer": {
        "interval": 150,
        "frames": [
            "-",
            "=",
            "≡"
        ]
    },
    "betaWave": {
        "interval": 80,
        "frames": [
            "ρββββββ",
            "βρβββββ",
            "ββρββββ",
            "βββρβββ",
            "ββββρββ",
            "βββββρβ",
            "ββββββρ"
        ]
    }
}