Full Code of janlelis/whirly for AI

main 336e8fc4288c cached
24 files
60.0 KB
26.8k tokens
25 symbols
1 requests
Download .txt
Repository: janlelis/whirly
Branch: main
Commit: 336e8fc4288c
Files: 24
Total size: 60.0 KB

Directory structure:
gitextract_4v6ma7m5/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── MIT-LICENSE.txt
├── README.md
├── Rakefile
├── data/
│   ├── cli-spinners.json
│   └── whirly-static-spinners.json
├── examples/
│   ├── all_spinners.rb
│   ├── asciinema_bundled_spinners.rb
│   ├── euruko.rb
│   ├── multi_lines.rb
│   ├── single.rb
│   └── status.rb
├── lib/
│   ├── whirly/
│   │   ├── spinners/
│   │   │   ├── cli.rb
│   │   │   └── whirly.rb
│   │   ├── spinners.rb
│   │   └── version.rb
│   └── whirly.rb
├── spec/
│   └── whirly_spec.rb
└── whirly.gemspec

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/test.yml
================================================
name: Test

on: [push, pull_request]

jobs:
  test:
    name: Ruby ${{ matrix.ruby }} (${{ matrix.os }})
    if: "!contains(github.event.head_commit.message, '[skip ci]')"
    strategy:
      matrix:
        ruby:
        - '4.0'
        - '3.4'
        - '3.3'
        - '3.2'
        - '3.1'
        - '3.0'
        - 'jruby'
        os:
        - ubuntu-latest
        - macos-latest
    runs-on: ${{matrix.os}}
    steps:
    - uses: actions/checkout@v6
    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: ${{matrix.ruby}}
        bundler-cache: true
    - name: Run tests
      run: bundle exec rake

  # test-windows:
  #   name: Ruby ${{ matrix.ruby }} (windows-latest)
  #   if: "!contains(github.event.head_commit.message, '[skip ci]')"
  #   strategy:
  #     matrix:
  #       ruby:
  #       - 3.0
  #       - 2.7
  #       - 2.6
  #   runs-on: windows-latest
  #   steps:
  #   - uses: actions/checkout@v2
  #   - name: Set up Ruby
  #     uses: ruby/setup-ruby@v1
  #     with:
  #       ruby-version: ${{matrix.ruby}}
  #       bundler-cache: true
  #   - run: cinst ansicon
  #   - name: Run tests
  #     run: bundle exec rake


================================================
FILE: .gitignore
================================================
Gemfile.lock
/pkg


================================================
FILE: .gitmodules
================================================
[submodule "data/external/cli-spinners"]
	path = data/external/cli-spinners
	url = https://github.com/sindresorhus/cli-spinners.git


================================================
FILE: CHANGELOG.md
================================================
## CHANGELOG

### 0.4.0

- Allow Ruby 4.x
- Add json (default) gem as dependency
- Update CLI spinners to 3.3.0

### 0.3.0

- Allow more recent versions of Ruby and unicode-display_width gem
- Update CLI spinners to 2.6.0

### 0.2.6

- Update CLI spinners to 1.1.0 (adds "weather" and "christmas")

### 0.2.5

- Update CLI spinners to 1.0.1

### 0.2.4

- Fix bug that the Whirly thread will also stop when main thread throws error
  (patch by @monkbroc)
- New spinner: xberg

### 0.2.3

- Fix bug that in some cases whirly output would be shown on non-ttys
- New spinners: card, cloud, photo, banknote, white_square

### 0.2.2

- More emotions for whirly (the spinner)
- Add cat spinner

### 0.2.1

- Use macOS terminal app compatible ANSI sequences

### 0.2.0

- Make paint dependency optional
- Remove pause feature
- Separate configuring into its own method, remember whirly's configuration, can be cleared with the new .reset method
- Introduce "stop" frames to display when spinner is over
- Different newline behaviour; append newline by default after spinner ran. Use position: "below" for old behaviour
- Support multiple frame modes: "linear", "random", "reverse", "swing"
- Proper unrendering (use unicode-display\_width)
- Introduce spinner packs (to deal with eventual name conflicts, currently: whirly + cli)
- Add more bundled spinners
- Update CLI spinners to v0.3.0 (two new spinners)
- Rename option :use\_color to just :color
- Option to set spinner can also take frames or proc directly
- Add ANSI escape mode option
- Add remove\_after\_stop option

### 0.1.1

- `non_tty` option to force TTY behaviour (whirly deactivates itself for non TTY by default)
- Allow passing in spinner hashes instead of only spinner names

### 0.1.0

- Initial release



================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at opensource@janlelis.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: Gemfile
================================================
source 'https://rubygems.org'

gemspec

gem 'minitest'
gem 'paint'
gem 'rake'
gem 'stringio'


================================================
FILE: MIT-LICENSE.txt
================================================
Copyright (c) 2016 Jan Lelis, https://janlelis.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.


================================================
FILE: README.md
================================================
# Whirly 😀 [![[version]](https://badge.fury.io/rb/whirly.svg)](https://badge.fury.io/rb/whirly)  [<img src="https://github.com/janlelis/whirly/workflows/Test/badge.svg" />](https://github.com/janlelis/whirly/actions/workflows/test.yml)

A simple, colorful and customizable terminal spinner library for Ruby. It comes with 24 custom spinners and also includes those from the [cli-spinners](https://github.com/sindresorhus/cli-spinners) project.

## Demonstration

![](whirly.gif)

### Bundled Whirly Spinners

[Play on asciinema](https://asciinema.org/a/88198?size=big)

### Bundled Spinners from CLI Spinners

![](https://raw.githubusercontent.com/sindresorhus/cli-spinners/main/screenshot.gif)

[Play on asciinema](https://asciinema.org/a/9mlcoussb137m32swwuqtb2p1?size=big)

## Setup

Add to your `Gemfile`:

```ruby
gem 'whirly'
gem 'paint' # makes whirly colorful (recommended)
```

## Usage

### Basic Usage

The spinner is shown while the block executes:

```ruby
Whirly.start do
  # do the heavy work here
  sleep 5
end
```

You can update the spinner text from inside the block:

```ruby
Whirly.start do
  Whirly.status = "Set some text to display alongside the spinner symbol"
  sleep 3
  Whirly.status = "Update it"
  sleep 2
end
```

If you want to avoid the block syntax, you can also stop it manually:

```ruby
Whirly.start
sleep 5
Whirly.stop
```

The `start` method takes a lot of options, like which spinner to use or an initial status. See further below for the full description of available options.

```ruby
Whirly.start spinner: "pong", color: false, status: "The Game of Pong" do
  sleep 10
end
```

Also see the [examples directory](https://github.com/janlelis/whirly/tree/main/examples) for example scripts.

### Configuring Whirly

You can pass the same options you would pass to `.start` to `.configure` instead to create a persistent configuration that will be used by `.start`:

```ruby
Whirly.configure spinner: "dots"

Whirly.start do
  sleep 3 # will use dots
end

Whirly.start do
  sleep 3 # will use dots again
end
```

Call `.reset` to restore unconfigured behaviour:

```ruby
Whirly.configure spinner: "dots"

Whirly.reset

Whirly.start do
  sleep 3 # will use default spinner
end
```

## Spinners

### Included Spinners

See [`data/whirly-static-spinnes.json`](https://github.com/janlelis/whirly/blob/main/data/whirly-static-spinners.json), [`lib/whirly/spinners/whirly.rb`](https://github.com/janlelis/whirly/blob/main/lib/whirly/spinners/whirly.rb) and [cli-spinners](https://github.com/sindresorhus/cli-spinners). You can get a demonstration of all bundled spinners by running the [`examples/all_spinners.rb`](https://github.com/janlelis/whirly/blob/main/examples/all_spinners.rb) script.

## All `Whirly.start` / `Whirly.configure` Configuration Options

### Main Options

#### `spinner:`

*Default:* `"whirly"`

You have multiple ways of telling *Whirly* which spinner should be used. You can pass the following to the `spinner:` option:

- The name of a bundled spinner
- An array of spinner frames to use
- A proc which generates the frames dynamically
- A full spinner hash object ([explained below](https://github.com/janlelis/whirly#full-spinner-hash-format))

#### `status:`

*Default:* None

Allows you to directly set the first status text to display alongside the spinner icon.

#### `interval:`

*Default:* `100`

The number of milliseconds between changing to the next spinner icon frame.

### Advanced Options

#### `ambiguous_characters_width:`

*Default:* `1`

If set to `2`, ambiguous Unicode charatcers will be treated as 2 colums wide. See [unicode-display_width](https://github.com/janlelis/unicode-display_width) for more details.

#### `ansi_escape_mode:`

*Default:* `"restore"`

Can be set to `"line"` to use an different way of producing ANSI escape sequences necessary (experimental).

#### `append_newline:`

*Default:* `true`

When the Whirly block is over (or `.stop` was called), a `"\n"` will be outputted. Change to `false` to prevent this.

#### `color:`

*Default:* `!!defined?(Paint)`

This option is responsible for displaying the spinner icon in random colors. Set to `false` if you do not want this. Related option `:color_change_rate`.

#### `color_change_rate:`

*Default:* `30`

A value which describes how fast the color of the spinner icon changes.

#### `hide_cursor:`

*Default:* `true`

By default, the terminal cursor gets hidden while displaying the spinner. This also registers an `at_exit` callback, which always restores the cursor when exitting the program. If you do not want to hide the cursor, change this option to `false`.

#### `mode:`

*Default:* `"linear"`

Instructs Whirly to play the frames in a different order. Possible values: `"linear"`, `"reverse"`, `"swing"`, and `"random"`. See [spinner format section](https://github.com/janlelis/whirly#mode-1) for more details.

#### `non_tty:`

*Default:* `false`

Whirly only gets activated if the current process appears to be a real terminal. If you want to activate it for non-terimnals, set this option to `true`.

#### `position:`

*Default:* `"normal"`

You can set this to `"below"` to let Whirly appear one line below its normal position.

#### `remove_after_stop:`

*Default:* `false`

Causes the last frame to be removed after the spinner stopped.

#### `stop:`

*Default:* None

You can pass a custom frame to be used to end the animation, for example:

```ruby
Whirly.start spinner: "clock", interval: 1000, stop: "⏰" do
  sleep 12
end
```

#### `spinner_packs:`

*Default:* `[:whirly, :cli]`

Whirly comes with spinners from different sources. This options defines which sources to consider (the value refers to an uppercased child constant of `Whirly::Spinners`) and in which order.

#### `stream:`

*Default:* `$stdout`

You can pass in an [IO](https://ruby-doc.org/core/IO.html)-like object, if you want to display *Whirly* on an other stream than `$stdout`.

## Full Spinner Hash Format

A full spinner is defined by a hash which can have the following key-value pairs. Please note that in order to keep the format more portable, all keys are strings and not Ruby symbols. Except for `"frames"` and `"proc"`, all options are overwritable when starting/configuring Whirly. See the included spinners for example definitions of spinners.

### `"frames"`

An [Array](https://ruby-doc.org/core/Array.html) or [Enumerable](https://ruby-doc.org/core/Enumerable.html) of strings that will be used as the spinner icon.

### `"proc"`

Instead of using `"frames"`: A proc which will generate the next frame with each call.

### `"interval"`

The number of milliseconds between changing to the next spinner icon frame.

### `"mode"`

The order in which frames should be played. It can be one of the following:

- `"linear"`: Cycle through all frames in normal order
- `"reverse"`: Cycle through all frames in reverse order
- `"swing"`: Cycle through all frames in normal order, and then in reverse order, but only play first and last frame once each round
- `"random"`: Play random frames

Please note: While `"linear"` also works with frames that are just an [Enumerable](https://ruby-doc.org/core/Enumerable.html), all other frame modes require the object to be representable as an [Array](https://ruby-doc.org/core/Array.html).

### `"stop"`

A frame to be used to end the spinner icon animation.

## Remarks, Troubleshooting, Caveats

- Interval is milliseconds, but don't rely on exact timing
- Will not do anything if stream is not a real console (or `non_tty: true` is passed)
- Colors not working? Be sure to include the [paint](https://github.com/janlelis/paint/) gem in your Gemfile
- Don't set very short intervals (or it might affect performance substantially)

## MIT License

- Copyright (C) 2016 Jan Lelis <https://janlelis.com>. Released under the MIT license.
- Contains data from cli-spinners:  MIT License, Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)


================================================
FILE: Rakefile
================================================
# # #
# Get gemspec info

gemspec_file = Dir['*.gemspec'].first
gemspec = eval File.read(gemspec_file), binding, gemspec_file
info = "#{gemspec.name} | #{gemspec.version} | " \
       "#{gemspec.runtime_dependencies.size} dependencies | " \
       "#{gemspec.files.size} files"


# # #
# Gem build and install task

desc info
task :gem do
  puts info + "\n\n"
  print "  "; sh "gem build #{gemspec_file}"
  FileUtils.mkdir_p 'pkg'
  FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg'
  puts; sh %{gem install --no-document pkg/#{gemspec.name}-#{gemspec.version}.gem}
end


# # #
# Start an IRB session with the gem loaded

desc "#{gemspec.name} | IRB"
task :irb do
  sh "irb -I ./lib -r #{gemspec.name.gsub '-','/'}"
end


# # #
# Run Specs

desc "#{gemspec.name} | Spec"
task :spec do
  ruby "spec/whirly_spec.rb"
end
task default: :spec


# # #
# Update spinners

desc "Update spinners"
task :update_spinners do
  sh "git submodule update --recursive --remote"
  cp "data/external/cli-spinners/spinners.json", "data/cli-spinners.json"
end


# # #
# Record ASCIICAST

desc "Record an asciicast via asciinema"
task :record_acsiicast do
  sh "cd && asciinema rec whirly-bundled-spinners-v0.2.0.json --title='Whirly v0.2.0 Bundled Spinners' --command='ruby #{File.dirname(__FILE__)}/examples/asciinema_bundled_spinners.rb'"
end



================================================
FILE: data/cli-spinners.json
================================================
{
	"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": [
			"⢀⠀",
			"⡀⠀",
			"⠄⠀",
			"⢂⠀",
			"⡂⠀",
			"⠅⠀",
			"⢃⠀",
			"⡃⠀",
			"⠍⠀",
			"⢋⠀",
			"⡋⠀",
			"⠍⠁",
			"⢋⠁",
			"⡋⠁",
			"⠍⠉",
			"⠋⠉",
			"⠋⠉",
			"⠉⠙",
			"⠉⠙",
			"⠉⠩",
			"⠈⢙",
			"⠈⡙",
			"⢈⠩",
			"⡀⢙",
			"⠄⡙",
			"⢂⠩",
			"⡂⢘",
			"⠅⡘",
			"⢃⠨",
			"⡃⢐",
			"⠍⡐",
			"⢋⠠",
			"⡋⢀",
			"⠍⡁",
			"⢋⠁",
			"⡋⠁",
			"⠍⠉",
			"⠋⠉",
			"⠋⠉",
			"⠉⠙",
			"⠉⠙",
			"⠉⠩",
			"⠈⢙",
			"⠈⡙",
			"⠈⠩",
			"⠀⢙",
			"⠀⡙",
			"⠀⠩",
			"⠀⢘",
			"⠀⡘",
			"⠀⠨",
			"⠀⢐",
			"⠀⡐",
			"⠀⠠",
			"⠀⢀",
			"⠀⡀"
		]
	},
	"dots13": {
		"interval": 80,
		"frames": [
			"⣼",
			"⣹",
			"⢻",
			"⠿",
			"⡟",
			"⣏",
			"⣧",
			"⣶"
		]
	},
	"dots14": {
		"interval": 80,
		"frames": [
			"⠉⠉",
			"⠈⠙",
			"⠀⠹",
			"⠀⢸",
			"⠀⣰",
			"⢀⣠",
			"⣀⣀",
			"⣄⡀",
			"⣆⠀",
			"⡇⠀",
			"⠏⠀",
			"⠋⠁"
		]
	},
	"dots8Bit": {
		"interval": 80,
		"frames": [
			"⠀",
			"⠁",
			"⠂",
			"⠃",
			"⠄",
			"⠅",
			"⠆",
			"⠇",
			"⡀",
			"⡁",
			"⡂",
			"⡃",
			"⡄",
			"⡅",
			"⡆",
			"⡇",
			"⠈",
			"⠉",
			"⠊",
			"⠋",
			"⠌",
			"⠍",
			"⠎",
			"⠏",
			"⡈",
			"⡉",
			"⡊",
			"⡋",
			"⡌",
			"⡍",
			"⡎",
			"⡏",
			"⠐",
			"⠑",
			"⠒",
			"⠓",
			"⠔",
			"⠕",
			"⠖",
			"⠗",
			"⡐",
			"⡑",
			"⡒",
			"⡓",
			"⡔",
			"⡕",
			"⡖",
			"⡗",
			"⠘",
			"⠙",
			"⠚",
			"⠛",
			"⠜",
			"⠝",
			"⠞",
			"⠟",
			"⡘",
			"⡙",
			"⡚",
			"⡛",
			"⡜",
			"⡝",
			"⡞",
			"⡟",
			"⠠",
			"⠡",
			"⠢",
			"⠣",
			"⠤",
			"⠥",
			"⠦",
			"⠧",
			"⡠",
			"⡡",
			"⡢",
			"⡣",
			"⡤",
			"⡥",
			"⡦",
			"⡧",
			"⠨",
			"⠩",
			"⠪",
			"⠫",
			"⠬",
			"⠭",
			"⠮",
			"⠯",
			"⡨",
			"⡩",
			"⡪",
			"⡫",
			"⡬",
			"⡭",
			"⡮",
			"⡯",
			"⠰",
			"⠱",
			"⠲",
			"⠳",
			"⠴",
			"⠵",
			"⠶",
			"⠷",
			"⡰",
			"⡱",
			"⡲",
			"⡳",
			"⡴",
			"⡵",
			"⡶",
			"⡷",
			"⠸",
			"⠹",
			"⠺",
			"⠻",
			"⠼",
			"⠽",
			"⠾",
			"⠿",
			"⡸",
			"⡹",
			"⡺",
			"⡻",
			"⡼",
			"⡽",
			"⡾",
			"⡿",
			"⢀",
			"⢁",
			"⢂",
			"⢃",
			"⢄",
			"⢅",
			"⢆",
			"⢇",
			"⣀",
			"⣁",
			"⣂",
			"⣃",
			"⣄",
			"⣅",
			"⣆",
			"⣇",
			"⢈",
			"⢉",
			"⢊",
			"⢋",
			"⢌",
			"⢍",
			"⢎",
			"⢏",
			"⣈",
			"⣉",
			"⣊",
			"⣋",
			"⣌",
			"⣍",
			"⣎",
			"⣏",
			"⢐",
			"⢑",
			"⢒",
			"⢓",
			"⢔",
			"⢕",
			"⢖",
			"⢗",
			"⣐",
			"⣑",
			"⣒",
			"⣓",
			"⣔",
			"⣕",
			"⣖",
			"⣗",
			"⢘",
			"⢙",
			"⢚",
			"⢛",
			"⢜",
			"⢝",
			"⢞",
			"⢟",
			"⣘",
			"⣙",
			"⣚",
			"⣛",
			"⣜",
			"⣝",
			"⣞",
			"⣟",
			"⢠",
			"⢡",
			"⢢",
			"⢣",
			"⢤",
			"⢥",
			"⢦",
			"⢧",
			"⣠",
			"⣡",
			"⣢",
			"⣣",
			"⣤",
			"⣥",
			"⣦",
			"⣧",
			"⢨",
			"⢩",
			"⢪",
			"⢫",
			"⢬",
			"⢭",
			"⢮",
			"⢯",
			"⣨",
			"⣩",
			"⣪",
			"⣫",
			"⣬",
			"⣭",
			"⣮",
			"⣯",
			"⢰",
			"⢱",
			"⢲",
			"⢳",
			"⢴",
			"⢵",
			"⢶",
			"⢷",
			"⣰",
			"⣱",
			"⣲",
			"⣳",
			"⣴",
			"⣵",
			"⣶",
			"⣷",
			"⢸",
			"⢹",
			"⢺",
			"⢻",
			"⢼",
			"⢽",
			"⢾",
			"⢿",
			"⣸",
			"⣹",
			"⣺",
			"⣻",
			"⣼",
			"⣽",
			"⣾",
			"⣿"
		]
	},
	"dotsCircle": {
		"interval": 80,
		"frames": [
			"⢎ ",
			"⠎⠁",
			"⠊⠑",
			"⠈⠱",
			" ⡱",
			"⢀⡰",
			"⢄⡠",
			"⢆⡀"
		]
	},
	"sand": {
		"interval": 80,
		"frames": [
			"⠁",
			"⠂",
			"⠄",
			"⡀",
			"⡈",
			"⡐",
			"⡠",
			"⣀",
			"⣁",
			"⣂",
			"⣄",
			"⣌",
			"⣔",
			"⣤",
			"⣥",
			"⣦",
			"⣮",
			"⣶",
			"⣷",
			"⣿",
			"⡿",
			"⠿",
			"⢟",
			"⠟",
			"⡛",
			"⠛",
			"⠫",
			"⢋",
			"⠋",
			"⠍",
			"⡉",
			"⠉",
			"⠑",
			"⠡",
			"⢁"
		]
	},
	"line": {
		"interval": 130,
		"frames": [
			"-",
			"\\",
			"|",
			"/"
		]
	},
	"line2": {
		"interval": 100,
		"frames": [
			"⠂",
			"-",
			"–",
			"—",
			"–",
			"-"
		]
	},
	"rollingLine": {
		"interval": 80,
		"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": [
			"◢",
			"◣",
			"◤",
			"◥"
		]
	},
	"binary": {
		"interval": 80,
		"frames": [
			"010010",
            "001100",
            "100101",
            "111010",
            "111101",
            "010111",
			"101011",
			"111000",
			"110011",
			"110101"
		]
	},
	"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": [
			"ρββββββ",
			"βρβββββ",
			"ββρββββ",
			"βββρβββ",
			"ββββρββ",
			"βββββρβ",
			"ββββββρ"
		]
	},
	"fingerDance": {
		"interval": 160,
		"frames": [
			"🤘 ",
			"🤟 ",
			"🖖 ",
			"✋ ",
			"🤚 ",
			"👆 "
		]
	},
	"fistBump": {
		"interval": 80,
		"frames": [
			"🤜\u3000\u3000\u3000\u3000🤛 ",
			"🤜\u3000\u3000\u3000\u3000🤛 ",
			"🤜\u3000\u3000\u3000\u3000🤛 ",
			"\u3000🤜\u3000\u3000🤛\u3000 ",
			"\u3000\u3000🤜🤛\u3000\u3000 ",
			"\u3000🤜✨🤛\u3000\u3000 ",
			"🤜\u3000✨\u3000🤛\u3000 "
		]
	},
	"soccerHeader": {
		"interval": 80,
		"frames": [
			" 🧑⚽️       🧑 ",
			"🧑  ⚽️      🧑 ",
			"🧑   ⚽️     🧑 ",
			"🧑    ⚽️    🧑 ",
			"🧑     ⚽️   🧑 ",
			"🧑      ⚽️  🧑 ",
			"🧑       ⚽️🧑  ",
			"🧑      ⚽️  🧑 ",
			"🧑     ⚽️   🧑 ",
			"🧑    ⚽️    🧑 ",
			"🧑   ⚽️     🧑 ",
			"🧑  ⚽️      🧑 "
		]
	},
	"mindblown": {
		"interval": 160,
		"frames": [
			"😐 ",
			"😐 ",
			"😮 ",
			"😮 ",
			"😦 ",
			"😦 ",
			"😧 ",
			"😧 ",
			"🤯 ",
			"💥 ",
			"✨ ",
			"\u3000 ",
			"\u3000 ",
			"\u3000 "
		]
	},
	"speaker": {
		"interval": 160,
		"frames": [
			"🔈 ",
			"🔉 ",
			"🔊 ",
			"🔉 "
		]
	},
	"orangePulse": {
		"interval": 100,
		"frames": [
			"🔸 ",
			"🔶 ",
			"🟠 ",
			"🟠 ",
			"🔶 "
		]
	},
	"bluePulse": {
		"interval": 100,
		"frames": [
			"🔹 ",
			"🔷 ",
			"🔵 ",
			"🔵 ",
			"🔷 "
		]
	},
	"orangeBluePulse": {
		"interval": 100,
		"frames": [
			"🔸 ",
			"🔶 ",
			"🟠 ",
			"🟠 ",
			"🔶 ",
			"🔹 ",
			"🔷 ",
			"🔵 ",
			"🔵 ",
			"🔷 "
		]
	},
	"timeTravel": {
		"interval": 100,
		"frames": [
			"🕛 ",
			"🕚 ",
			"🕙 ",
			"🕘 ",
			"🕗 ",
			"🕖 ",
			"🕕 ",
			"🕔 ",
			"🕓 ",
			"🕒 ",
			"🕑 ",
			"🕐 "
		]
	},
	"aesthetic": {
		"interval": 80,
		"frames": [
			"▰▱▱▱▱▱▱",
			"▰▰▱▱▱▱▱",
			"▰▰▰▱▱▱▱",
			"▰▰▰▰▱▱▱",
			"▰▰▰▰▰▱▱",
			"▰▰▰▰▰▰▱",
			"▰▰▰▰▰▰▰",
			"▰▱▱▱▱▱▱"
		]
	},
	"dwarfFortress": {
		"interval": 80,
		"frames": [
			" ██████£££  ",
			"☺██████£££  ",
			"☺██████£££  ",
			"☺▓█████£££  ",
			"☺▓█████£££  ",
			"☺▒█████£££  ",
			"☺▒█████£££  ",
			"☺░█████£££  ",
			"☺░█████£££  ",
			"☺ █████£££  ",
			" ☺█████£££  ",
			" ☺█████£££  ",
			" ☺▓████£££  ",
			" ☺▓████£££  ",
			" ☺▒████£££  ",
			" ☺▒████£££  ",
			" ☺░████£££  ",
			" ☺░████£££  ",
			" ☺ ████£££  ",
			"  ☺████£££  ",
			"  ☺████£££  ",
			"  ☺▓███£££  ",
			"  ☺▓███£££  ",
			"  ☺▒███£££  ",
			"  ☺▒███£££  ",
			"  ☺░███£££  ",
			"  ☺░███£££  ",
			"  ☺ ███£££  ",
			"   ☺███£££  ",
			"   ☺███£££  ",
			"   ☺▓██£££  ",
			"   ☺▓██£££  ",
			"   ☺▒██£££  ",
			"   ☺▒██£££  ",
			"   ☺░██£££  ",
			"   ☺░██£££  ",
			"   ☺ ██£££  ",
			"    ☺██£££  ",
			"    ☺██£££  ",
			"    ☺▓█£££  ",
			"    ☺▓█£££  ",
			"    ☺▒█£££  ",
			"    ☺▒█£££  ",
			"    ☺░█£££  ",
			"    ☺░█£££  ",
			"    ☺ █£££  ",
			"     ☺█£££  ",
			"     ☺█£££  ",
			"     ☺▓£££  ",
			"     ☺▓£££  ",
			"     ☺▒£££  ",
			"     ☺▒£££  ",
			"     ☺░£££  ",
			"     ☺░£££  ",
			"     ☺ £££  ",
			"      ☺£££  ",
			"      ☺£££  ",
			"      ☺▓££  ",
			"      ☺▓££  ",
			"      ☺▒££  ",
			"      ☺▒££  ",
			"      ☺░££  ",
			"      ☺░££  ",
			"      ☺ ££  ",
			"       ☺££  ",
			"       ☺££  ",
			"       ☺▓£  ",
			"       ☺▓£  ",
			"       ☺▒£  ",
			"       ☺▒£  ",
			"       ☺░£  ",
			"       ☺░£  ",
			"       ☺ £  ",
			"        ☺£  ",
			"        ☺£  ",
			"        ☺▓  ",
			"        ☺▓  ",
			"        ☺▒  ",
			"        ☺▒  ",
			"        ☺░  ",
			"        ☺░  ",
			"        ☺   ",
			"        ☺  &",
			"        ☺ ☼&",
			"       ☺ ☼ &",
			"       ☺☼  &",
			"      ☺☼  & ",
			"      ‼   & ",
			"     ☺   &  ",
			"    ‼    &  ",
			"   ☺    &   ",
			"  ‼     &   ",
			" ☺     &    ",
			"‼      &    ",
			"      &     ",
			"      &     ",
			"     &   ░  ",
			"     &   ▒  ",
			"    &    ▓  ",
			"    &    £  ",
			"   &    ░£  ",
			"   &    ▒£  ",
			"  &     ▓£  ",
			"  &     ££  ",
			" &     ░££  ",
			" &     ▒££  ",
			"&      ▓££  ",
			"&      £££  ",
			"      ░£££  ",
			"      ▒£££  ",
			"      ▓£££  ",
			"      █£££  ",
			"     ░█£££  ",
			"     ▒█£££  ",
			"     ▓█£££  ",
			"     ██£££  ",
			"    ░██£££  ",
			"    ▒██£££  ",
			"    ▓██£££  ",
			"    ███£££  ",
			"   ░███£££  ",
			"   ▒███£££  ",
			"   ▓███£££  ",
			"   ████£££  ",
			"  ░████£££  ",
			"  ▒████£££  ",
			"  ▓████£££  ",
			"  █████£££  ",
			" ░█████£££  ",
			" ▒█████£££  ",
			" ▓█████£££  ",
			" ██████£££  ",
			" ██████£££  "
		]
	}
}


================================================
FILE: data/whirly-static-spinners.json
================================================
{
  "roman_numerals": {
    "interval": 90,
    "mode": "swing",
    "frames": [
      "Ⅰ",
      "Ⅱ",
      "Ⅲ",
      "Ⅳ",
      "Ⅴ",
      "Ⅵ",
      "Ⅶ",
      "Ⅷ",
      "Ⅸ",
      "Ⅹ"
    ]
  },
  "double_mark": {
    "interval": 120,
    "mode": "random",
    "frames": [
      "⁇",
      "⁈",
      "⁉",
      "‼"
    ]
  },
  "heart_exclamation": {
    "interval": 45,
    "frames": [
      "❢",
      "❣"
    ]
  },
  "pencil": {
    "interval": 200,
    "frames": [
      "✏",
      "✎"
    ]
  },
  "bars": {
    "interval": 80,
    "mode": "swing",
    "frames": [
      "𝍠",
      "𝍡",
      "𝍢",
      "𝍣",
      "𝍤"
    ]
  },
  "dice": {
    "interval": 100,
    "mode": "random",
    "frames": [
      "⚀",
      "⚁",
      "⚂",
      "⚃",
      "⚄",
      "⚅"
    ]
  },
  "hanoi": {
    "interval": 150,
    "mode": "swing",
    "frames": [
      "𝍥",
      "𝍦",
      "𝍧",
      "𝍨"
    ]
  },
  "vertical_bars": {
    "interval": 80,
    "mode": "swing",
    "frames": [
      "𝍩",
      "𝍪",
      "𝍫",
      "𝍬",
      "𝍭"
    ]
  },
  "whirly": {
    "interval": 200,
    "mode": "random",
    "frames": [
      "😀",
      "😁",
      "😂",
      "😃",
      "😄",
      "😅",
      "😆",
      "😇",
      "😈",
      "😉",
      "😊",
      "😋",
      "😌",
      "😍",
      "😎",
      "😏",
      "😐",
      "😑",
      "😒",
      "😓",
      "😔",
      "😕",
      "😖",
      "😗",
      "😘",
      "😙",
      "😚",
      "😛",
      "😜",
      "😝",
      "😞",
      "😟",
      "😠",
      "😡",
      "😢",
      "😣",
      "😤",
      "😥",
      "😦",
      "😧",
      "😨",
      "😩",
      "😪",
      "😫",
      "😬",
      "😭",
      "😮",
      "😯",
      "😰",
      "😱",
      "😲",
      "😳",
      "😴",
      "😵",
      "😶",
      "🙁",
      "🙂",
      "🙃",
      "🙄",
      "😷",
      "🤐",
      "🤑",
      "🤒",
      "🤓",
      "🤔",
      "🤕",
      "🤖",
      "🤗"
    ]
  },
  "cat": {
    "interval": 200,
    "mode": "random",
    "frames": [
      "😸",
      "😹",
      "😺",
      "😻",
      "😼",
      "😽",
      "😾",
      "😿",
      "🙀"
    ]
  },
  "card": {
    "interval": 90,
    "stop": "🂠",
    "frames": [
      "🃁", "🃂", "🃃", "🃄", "🃅", "🃆", "🃇", "🃈", "🃉", "🃊", "🃋", "🃌", "🃍", "🃎",
      "🂱", "🂲", "🂳", "🂴", "🂵", "🂶", "🂷", "🂸", "🂹", "🂺", "🂻", "🂼", "🂽", "🂾",
      "🂡", "🂢", "🂣", "🂤", "🂥", "🂦", "🂧", "🂨", "🂩", "🂪", "🂫", "🂬", "🂭", "🂮",
      "🃑", "🃒", "🃓", "🃔", "🃕", "🃖", "🃗", "🃘", "🃙", "🃚", "🃛", "🃜", "🃝", "🃞"
    ]
  },
  "cloud": {
    "interval": 140,
    "frames": [
      "🌥",
      "🌦",
      "🌧",
      "🌨",
      "🌩",
      "🌪"
    ]
  },
  "photo": {
    "interval": 200,
    "frames": [
      "📷",
      "📸"
    ]
  },
  "banknote": {
    "interval": 100,
    "frames": [
      "💴",
      "💵",
      "💶",
      "💷"
    ]
  },
  "white_square": {
    "interval": 100,
    "mode": "swing",
    "frames": [
      "🞓",
      "🞒",
      "🞑",
      "🞐",
      "🞏",
      "🞎",
      "🞔"
    ]
  },
  "xberg": {
    "interval": 150,
    "mode": "random",
    "frames": [
      "⨯",
      "⛰",
      "⛰",
      "⛰",
      "⛰",
      "⛰",
      "⛰"
    ]
  },
  "circled_letter": {
    "interval": 120,
    "mode": "random",
    "frames": [
      "Ⓐ", "Ⓑ", "Ⓒ", "Ⓓ", "Ⓔ", "Ⓕ", "Ⓖ", "Ⓗ", "Ⓘ",
      "Ⓙ", "Ⓚ", "Ⓛ", "Ⓜ", "Ⓝ", "Ⓞ", "Ⓟ", "Ⓠ", "Ⓡ",
      "Ⓢ", "Ⓣ", "Ⓤ", "Ⓥ", "Ⓦ", "Ⓧ", "Ⓨ", "Ⓩ"
    ]
  },
  "circled_number": {
    "interval": 120,
    "mode": "random",
    "frames": [
      "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨"
    ]
  },
  "letter_with_parens": {
    "interval": 150,
    "mode": "random",
    "frames": [
      "🄐", "🄑", "🄒", "🄓", "🄔", "🄕", "🄖", "🄗", "🄘",
      "🄙", "🄚", "🄛", "🄜", "🄝", "🄞", "🄟", "🄠", "🄡",
      "🄢", "🄣", "🄤", "🄥", "🄦", "🄧", "🄨", "🄩"
    ]
  },
  "starlike": {
    "interval": 120,
    "mode": "random",
    "frames": [
      "✩", "✪", "✫", "✬", "✭", "✮", "✯", "✰",
      "✱", "✲", "✳", "✴", "✵", "✶", "✷", "✸",
      "✹", "✺", "✻", "✼", "✽", "✾", "✿", "❀",
      "❁", "❂", "❃", "❄", "❅", "❆", "❇", "❈",
      "❉", "❊"
    ]
  }
}


================================================
FILE: examples/all_spinners.rb
================================================
require_relative "../lib/whirly"
require "paint"

# Demonstrates all available spinners

if spinner_pack = $*[0]
  constants = [spinner_pack.upcase]
else
  constants = Whirly::Spinners.constants
end

constants.each{ |spinner_pack|
  puts
  puts Paint[spinner_pack, :underline]
  puts
  Whirly::Spinners.const_get(spinner_pack).keys.sort.each{ |spinner_name|
    Whirly.start(spinner: spinner_name, status: spinner_name){
      sleep 1.5
    }
  }
}


================================================
FILE: examples/asciinema_bundled_spinners.rb
================================================
require_relative "../lib/whirly"
require "paint"

system "clear"

Whirly::Spinners::WHIRLY.keys.sort.each{ |spinner_name|
  Whirly.start(spinner: spinner_name, status: spinner_name, append_newline: false, ansi_escape_mode: "line", remove_after_stop: true){
    sleep 1.5
  }
}

system "exit"


================================================
FILE: examples/euruko.rb
================================================
require_relative '../lib/whirly'
require 'paint'

# Lightning talk at EuRuKo 2016

# # # Whirly

print "\033c"
puts Paint["Whirly", :underline]

Whirly.start status: 'Generating something huge…' do
  sleep 15
  Whirly.status = "(actually it's just `sleep 15`)"
  sleep 15
  Whirly.status = "Almost done…"
  sleep 3
  Whirly.status = "10 more seconds!"
  sleep 10
end

puts
puts
puts
puts "Done"
sleep 5

# # # Earth

print "\033c"
puts Paint["Earth Spinner", :underline]

Whirly.start spinner: "earth"
Whirly.status = "Travelling…"
sleep 9
Whirly.stop

puts
puts
puts
puts "Done"
sleep 5

# # # Pong Game

print "\033c"
puts Paint["Pong", :underline]

Whirly.start spinner: "pong", color: false, status: "Two computers in a game of Pong" do
  sleep 9
end

puts
puts
puts
puts "Done"
sleep 5

# # # Ticking Clock

print "\033c"
puts Paint["Clock", :underline]

Whirly.start spinner: "clock", interval: 1000 do
  sleep 12
end

puts
puts
puts
puts "Time is over"

# # # URL

print "\033c"
puts Paint["Get WHIRLY", :bold]

Whirly.start spinner: "whirly", status: "https://github.com/janlelis/whirly" do
  sleep 60
end


================================================
FILE: examples/multi_lines.rb
================================================
require_relative "../lib/whirly"
require "paint"

# Demonstrate the look of using multiple spinners

Whirly.configure(spinner: "dots", stop: "✔")

Whirly.start status: "Processing" do
  sleep 2
end

Whirly.start status: "More processing" do
  sleep 2
end

Whirly.start status: "Even more processing" do
  sleep 2
end

puts "Done"


================================================
FILE: examples/single.rb
================================================
require_relative "../lib/whirly"
require "paint"

# Call a single spinner from the command-line

Whirly.start(spinner: $*[0], status: $*[2] || $*[0], color: false){ sleep(($*[1] || 10).to_i) }


================================================
FILE: examples/status.rb
================================================
require_relative "../lib/whirly"

Whirly.start status: "Initial status, passed when starting Whirly"
sleep 3
Whirly.status = "Status update"
sleep 3
Whirly.stop


================================================
FILE: lib/whirly/spinners/cli.rb
================================================
require "json"

module Whirly
  module Spinners
    CLI = JSON.load(File.read(File.dirname(__FILE__) + "/../../../data/cli-spinners.json")).freeze
  end
end


================================================
FILE: lib/whirly/spinners/whirly.rb
================================================
require "json"

module Whirly
  module Spinners
    WHIRLY = {
      "random_dots"         => { "proc" => ->(){ [ 0x2800 + rand(256)].pack("U") }, "interval" => 100 },
      "mahjong"             => { "proc" => ->(){ [0x1F000 + rand(44)].pack("U") }, "interval" => 200 },
      "domino"              => { "proc" => ->(){ [0x1F030 + rand(50)].pack("U") }, "interval" => 200 },
      "vertical_domino"     => { "proc" => ->(){ [0x1F062 + rand(50)].pack("U") }, "interval" => 200 }
    }
    WHIRLY.merge! JSON.load(File.read(File.dirname(__FILE__) + "/../../../data/whirly-static-spinners.json"))

    WHIRLY.freeze
  end
end


================================================
FILE: lib/whirly/spinners.rb
================================================
module Whirly
  module Spinners
  end
end

require_relative "spinners/whirly"
require_relative "spinners/cli"


================================================
FILE: lib/whirly/version.rb
================================================
# frozen_string_literal: true

module Whirly
  VERSION = "0.4.0"
end


================================================
FILE: lib/whirly.rb
================================================
require_relative "whirly/version"
require_relative "whirly/spinners"

require "unicode/display_width"

begin
  require "paint"
rescue LoadError
end

module Whirly
  @configured = false

  CLI_COMMANDS = {
    hide_cursor: "\x1b[?25l",
    show_cursor: "\x1b[?25h",
  }.freeze

  DEFAULT_OPTIONS = {
    ambiguous_character_width: 1,
    ansi_escape_mode: "restore",
    append_newline: true,
    color: !!defined?(Paint),
    color_change_rate: 30,
    hide_cursor: true,
    non_tty: false,
    position: "normal",
    remove_after_stop: false,
    spinner: "whirly",
    spinner_packs: [:whirly, :cli],
    status: nil,
    stream: $stdout,
  }.freeze

  SOFT_DEFAULT_OPTIONS = {
    interval: 100,
    mode: "linear",
    stop: nil,
  }.freeze

  class << self
    attr_accessor :status
    attr_reader :options

    def enabled?
      !!(defined?(@enabled) && @enabled)
    end

    def configured?
      !!(@configured)
    end
  end

  # set spinner directly or lookup
  def self.configure_spinner(spinner_option)
    case spinner_option
    when Hash
      spinner = spinner_option.dup
    when Enumerable
      spinner = { "frames" => spinner_option.dup }
    when Proc
      spinner = { "proc" => spinner_option.dup }
    else
      spinner = nil
      catch(:found){
        @options[:spinner_packs].each{ |spinner_pack|
          spinners = Whirly::Spinners.const_get(spinner_pack.to_s.upcase)
          if spinners[spinner_option]
            spinner = spinners[spinner_option].dup
            throw(:found)
          end
        }
      }
    end

    # validate spinner
    if !spinner || (!spinner["frames"] && !spinner["proc"])
      raise(ArgumentError, "Whirly: Invalid spinner given")
    end

    spinner
  end

  # frames can be generated from enumerables or procs
  def self.configure_frames(spinner)
    if spinner["frames"]
      case spinner["mode"]
      when "swing"
        frames = (spinner["frames"].to_a + spinner["frames"].to_a[1..-2].reverse).cycle
      when "random"
        frame_pool = spinner["frames"].to_a
        frames = ->(){ frame_pool.sample }
      when "reverse"
        frames = spinner["frames"].to_a.reverse.cycle
      else
        frames = spinner["frames"].cycle
      end
    elsif spinner["proc"]
      frames = spinner["proc"].dup
    else
      raise(ArgumentError, "Whirly: Invalid spinner given")
    end

    if frames.is_a? Proc
      class << frames
        alias next call
      end
    end

    frames
  end

  # save options and preprocess, set defaults if value is still unknown
  def self.configure(**options)
    if !defined?(@configured) || !@configured || !defined?(@options) || !@options
      @options = DEFAULT_OPTIONS.dup
      @configured = true
    end

    @options.merge!(options)

    spinner = configure_spinner(@options[:spinner])
    spinner_overwrites = {}
    spinner_overwrites["mode"] = @options[:mode] if @options.key?(:mode)
    @frames   = configure_frames(spinner.merge(spinner_overwrites))

    @interval = (@options[:interval] || spinner["interval"] || SOFT_DEFAULT_OPTIONS[:interval]) * 0.001
    @stop     = @options[:stop] || spinner["stop"]
    @status   = @options[:status]
  end

  def self.start(**options)
    # optionally overwrite configuration on start
    configure(**options)

    # only enable once
    return false if defined?(@enabled) && @enabled

    # set status to enabled
    @enabled = true

    # only do something if we are on a real terminal (or forced)
    return false unless @options[:stream].tty? || @options[:non_tty]

    # ensure cursor is visible after exit the program (only register for the very first time)
    if (!defined?(@at_exit_handler_registered) || !@at_exit_handler_registered) && @options[:hide_cursor]
      @at_exit_handler_registered = true
      stream = @options[:stream]
      at_exit{ stream.print CLI_COMMANDS[:show_cursor] }
    end

    # init color
    initialize_color if @options[:color]

    # hide cursor
    @options[:stream].print CLI_COMMANDS[:hide_cursor] if @options[:hide_cursor]

    # start spinner loop
    @thread = Thread.new do
      @current_frame = nil
      while true # it's just a spinner, no exact timing here
        next_color if @color
        render
        sleep(@interval)
      end
    end

    # idiomatic block syntax support
    if block_given?
      begin
        yield
      ensure
        Whirly.stop
      end
    end

    true
  end

  def self.stop(stop_frame = nil)
    return false unless @enabled
    @enabled = false
    return false unless @options[:stream].tty? || @options[:non_tty]

    @thread.terminate if @thread
    render(stop_frame || @stop) if stop_frame || @stop
    unrender if @options[:remove_after_stop]
    @options[:stream].puts if @options[:append_newline]
    @options[:stream].print CLI_COMMANDS[:show_cursor] if @options[:hide_cursor]

    true
  end

  def self.reset
    at_exit_handler_registered = defined?(@at_exit_handler_registered) && @at_exit_handler_registered
    instance_variables.each{ |iv| remove_instance_variable(iv) }
    @at_exit_handler_registered = at_exit_handler_registered
    @configured = false
  end

  # - - -

  def self.unrender
    return unless @current_frame
    case @options[:ansi_escape_mode]
    when "restore"
      @options[:stream].print(render_prefix + (
          ' ' * (Unicode::DisplayWidth.of(@current_frame, @options[:ambiguous_character_width]) + 1)
      ) + render_suffix)
    when "line"
      @options[:stream].print "\e[1K"
    end
  end

  def self.render(next_frame = nil)
    unrender

    @current_frame = next_frame || @frames.next
    @current_frame = Paint[@current_frame, @color] if @options[:color]
    @current_frame += "  #{@status}" if @status

    @options[:stream].print(render_prefix + @current_frame.to_s + render_suffix)
  end

  def self.render_prefix
    res = ""
    res << "\n" if @options[:position] == "below"
    res << "\e7" if @options[:ansi_escape_mode] == "restore"
    res << "\e[G" if @options[:ansi_escape_mode] == "line"
    res
  end

  def self.render_suffix
    res = ""
    res << "\e8" if @options[:ansi_escape_mode] == "restore"
    res << "\e[1A" if @options[:position] == "below"
    res
  end

  def self.initialize_color
    if !defined?(Paint)
      warn "Whirly warning: Using colors requires the paint gem"
    else
      @color = "%.6x" % rand(16777216)
      @color_directions = (0..2).map{ |e| rand(3) - 1 }
    end
  end

  def self.next_color
    @color = @color.scan(/../).map.with_index{ |c, i|
      color_change = rand(@options[:color_change_rate]) * @color_directions[i]
      nc = c.to_i(16) + color_change
      if nc <= 0
        nc = 0
        @color_directions[i] = rand(3) - 1
      elsif nc >= 255
        nc = 255
        @color_directions[i] = rand(3) - 1
      end
      "%.2x" % nc
    }.join
  end
end


================================================
FILE: spec/whirly_spec.rb
================================================
require_relative "../lib/whirly"
require "minitest/autorun"
require "paint"
# require "irbtools/binding"
require "stringio"

def short_sleep
  sleep 0.4
end

def medium_sleep
  sleep 0.7
end

def long_sleep
  sleep 1
end

describe Whirly do
  before do
    Whirly.reset
    @capture = StringIO.new
    Whirly.configure(non_tty: true, stream: @capture)
  end

  describe "General Usage" do
    it "outputs every frame of the spinner" do
      spinner = { "frames" => ["first", "second", "third"], "interval" => 5 }

      Whirly.start(spinner: spinner)
      short_sleep
      Whirly.stop

      assert_match /first.*second.*third/m, @capture.string
    end

    it "calls spinner proc instead of frames if proc is given" do
      spinner = { "proc" => ->(){ "frame" }, "interval" => 5 }

      Whirly.start(spinner: spinner)
      short_sleep
      Whirly.stop

      assert_match /frame/, @capture.string
    end
  end

  describe "Status Updates" do
    it "shows status text alongside spinner" do
      Whirly.start
      Whirly.status = "Fetching…"
      medium_sleep
      Whirly.status = "Updates…"
      medium_sleep
      Whirly.stop

      assert_match /Fetching.*Updates…/m, @capture.string
    end

    it "shows initial status" do
      Whirly.start(status: "Initial")
      short_sleep
      Whirly.stop

      assert_match /Initial/, @capture.string
    end
  end

  describe "Finishing" do
    it "shows spinner finished frame if stop is set in spinner definition" do
      spinner = { "frames" => ["first", "second", "third"], "stop" => "STOP", "interval" => 5 }

      Whirly.start(spinner: spinner)
      short_sleep
      Whirly.stop

      assert_match /STOP/, @capture.string
    end

    it "shows spinner finished frame if stop frame is passed when stopping" do
      spinner = { "frames" => ["first", "second", "third"], "interval" => 5 }

      Whirly.start(spinner: spinner)
      short_sleep
      Whirly.stop("STOP")

      assert_match /STOP/, @capture.string
    end

    it "shows spinner finished frame if stop frame is passed when starting" do
      spinner = { "frames" => ["first", "second", "third"], "interval" => 5 }

      Whirly.start(spinner: spinner, stop: "STOP")
      short_sleep
      Whirly.stop

      assert_match /STOP/, @capture.string
    end

    it "appends newline when stopping" do
      Whirly.start(hide_cursor: false)
      short_sleep
      Whirly.stop

      assert_match /\n\z/, @capture.string
    end

    it "appends no newline when stopping when :append_newline option is false" do
      Whirly.start(hide_cursor: false, append_newline: false)
      short_sleep
      Whirly.stop

      assert_match /[^\n]\z/, @capture.string
    end

    it "removes the spinner after stopping when :remove_after_stop is true" do
      Whirly.start(hide_cursor: false, remove_after_stop: true)
      short_sleep
      Whirly.stop

      assert_match /\e8\n\z/, @capture.string
    end
  end

  describe "Spinner" do
    describe "Passing a Spinner" do
      it "can be the name of a bundled spinner (whirly-spinners)" do
        Whirly.start(spinner: "pencil")
        medium_sleep
        Whirly.stop

        assert_match /✎/, @capture.string
      end

      it "can be the name of a bundled spinner (cli-spinners)" do
        Whirly.start(spinner: "dots3")
        medium_sleep
        Whirly.stop

        assert_match /⠋/, @capture.string
      end

      it "can be an Array of frames" do
        Whirly.start(spinner: ["A", "B"])
        medium_sleep
        Whirly.stop

        assert_match /A.*B/m, @capture.string
      end

      it "can be an Enumerator of frames" do
        Whirly.start(spinner: "A".."B")
        medium_sleep
        Whirly.stop

        assert_match /A.*B/m, @capture.string
      end

      it "can be a Proc which generates frames" do
        Whirly.start(spinner: ->(){ "frame" })
        medium_sleep
        Whirly.stop

        assert_match /frame/m, @capture.string
      end
    end

    describe "Frame Mode" do
      it "can be set to random" do
        spinner = { "frames" => "A".."M", "mode" => "random", "interval" => 10 }

        Whirly.start(spinner: spinner)
        short_sleep
        Whirly.stop

        refute /\A.*?A.*?B.*?C.*?D.*?E.*?F.*?G.*?H.*?I.*?J.*?K.*?L.*?M/m =~ @capture.string
      end

      it "can be set to reverse" do
        spinner = { "frames" => "A".."H", "mode" => "reverse", "interval" => 10 }

        Whirly.start(spinner: spinner)
        medium_sleep
        Whirly.stop

        assert_match /H.*G.*F.*E.*D.*C.*B.*A/m, @capture.string
      end

      it "can be set to swing" do
        spinner = { "frames" => "A".."H", "mode" => "swing", "interval" => 10 }

        Whirly.start(spinner: spinner)
        medium_sleep
        Whirly.stop

        assert_match /A.*B.*C.*D.*E.*F.*G.*H.*G.*F.*E.*D.*C.*B.*A/m, @capture.string
      end
    end

    describe "Interval" do
      it "spins more often when interval is lower" do
        capture1 = StringIO.new
        Whirly.start(stream: capture1, interval: 100)
        long_sleep
        Whirly.stop

        capture2 = StringIO.new
        Whirly.start(stream: capture2, interval: 50)
        long_sleep
        Whirly.stop

        assert capture1.string.size < capture2.string.size
      end
    end
  end

  describe "Colors" do
    it "will use no color when :color option is falsey" do
      Whirly.start(color: false)
      short_sleep
      Whirly.stop

      refute /\[38;/ =~ @capture.string
    end

    it "will use color when :color option is truthy" do
      Whirly.start(color: true)
      short_sleep
      Whirly.stop

      assert /\[38;/ =~ @capture.string
    end

    it "defaults :color to true when the paint gem is available" do
      Whirly.reset
      Whirly.configure
      assert Whirly.options[:color]
    end

    # it "defaults :color to true when the paint gem is not available" do
    #   remember_paint = Paint
    #   Object.send(:remove_const, :Paint)
    #   Whirly.reset
    #   Whirly.configure
    #   Object.send(:const_set, :Paint, remember_paint)
    #   refute Whirly.options[:color]
    # end

    it "changes the the color" do
      Whirly.start
      long_sleep
      Whirly.stop

      colors = @capture.string.scan(/\[38;.*?m/).flatten
      assert colors.uniq.size > 1
    end
  end

  describe "Cursor" do
    it "hides (and later shows) cursor when :hide_cursor => true option is given (default)" do
      Whirly.start(hide_cursor: true)
      short_sleep
      Whirly.stop

      assert_match /\[?25l.*\[?25h/m, @capture.string
    end

    it "does not hide cursor when :hide_cursor => false option is given" do
      Whirly.start(hide_cursor: false)
      short_sleep
      Whirly.stop

      refute /\[?25l.*\[?25h/m =~ @capture.string
    end
  end

  describe "Spinner Packs" do
    it "can be passed an alternative set of :spinner_packs" do
      assert_raises ArgumentError do
        Whirly.start(spinner_packs: [:cli], spinner: "cat") # whirly is part of :whirly, but not of :cli
        Whirly.stop
      end
    end
  end

  describe "Ansi Escape Mode" do
    it "will use save and restore ANSI sequences as default (or when 'restore') is given" do
      Whirly.start
      short_sleep
      Whirly.stop
      assert_match /\e7.*\e8/m, @capture.string
    end

    it "will use beginning of line and clear line ANSI sequences when 'line' is given" do
      Whirly.start(ansi_escape_mode: 'line')
      medium_sleep
      Whirly.stop
      assert_match /\e\[G.*\e\[1K/m, @capture.string
    end
  end

  describe "Streams and TTYs" do
    it "will not output anything on non-ttys" do
      Whirly.reset
      @capture = StringIO.new
      Whirly.start(stream: @capture)
      short_sleep
      Whirly.stop
      assert_equal "", @capture.string
    end

    it "will output something on non-ttys when :non_tty => true option is given" do
      Whirly.reset
      @capture = StringIO.new
      Whirly.start(stream: @capture, non_tty: true)
      short_sleep
      Whirly.stop
      refute_equal "", @capture.string
    end

    it "can be configured to which stream whirly's output goes" do
      iolike = StringIO.new
      Whirly.start(stream: iolike, non_tty: true)
      short_sleep
      Whirly.stop
      refute_equal "", iolike.string
    end
  end

  describe "Positioning" do
    it "will render spinner 1 line further below (useful for spinning while git cloning)" do
      Whirly.start(position: "below")
      short_sleep
      Whirly.stop

      assert_match /\n.*\e\[1A/m, @capture.string
    end
  end

  describe "Configure and Reset" do
    it "can be configured before starting" do
      Whirly.configure spinner: "dots", interval: 5

      Whirly.start
      short_sleep
      Whirly.stop

      assert_match /⠧/, @capture.string
    end

    it "can be reset using .reset" do
      Whirly.configure spinner: "dots", interval: 5
      Whirly.reset

      Whirly.start(non_tty: true, stream: @capture)
      short_sleep
      Whirly.stop
      assert_match /\A[^⠧]+\z/, @capture.string
    end
  end

  describe ".enabled?" do
    it "returns false if whirly was not started yet" do
      refute_predicate Whirly, :enabled?
    end

    it "returns true if whirly was started, but not yet stopped" do
      Whirly.start
      assert_predicate Whirly, :enabled?
      Whirly.stop
    end

    it "returns false if whirly was stopped" do
      Whirly.start
      Whirly.stop
      refute_predicate Whirly, :enabled?
    end
  end

  describe "Error Handling" do
    it "stops the spinner, when the main thread threw an exception [gh#3]" do
      begin
        Whirly.start status: "working" do
          short_sleep
          raise 'error!'
        end
      rescue
      end

      refute_predicate Whirly, :enabled?
    end
  end
end


================================================
FILE: whirly.gemspec
================================================
# -*- encoding: utf-8 -*-

require File.dirname(__FILE__) + "/lib/whirly/version"

Gem::Specification.new do |gem|
  gem.name          = "whirly"
  gem.version       = Whirly::VERSION
  gem.summary       = "Whirly: The friendly terminal spinner"
  gem.description   = "Simple terminal spinner with support for custom spinners. Includes spinners from npm's cli-spinners."
  gem.authors       = ["Jan Lelis"]
  gem.email         = ["hi@ruby.consulting"]
  gem.homepage      = "https://github.com/janlelis/whirly"
  gem.license       = "MIT"

  gem.files         = Dir["{**/}{.*,*}"].select{ |path| File.file?(path) && path !~ /^(pkg|data)/ } + %w[
                        data/cli-spinners.json
                        data/whirly-static-spinners.json
                      ]
  gem.executables   = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
  gem.test_files    = gem.files.grep(%r{^(test|spec|features)/})
  gem.require_paths = ["lib"]

  gem.add_dependency "unicode-display_width", ">= 1.1"
  gem.add_dependency "json"

  gem.required_ruby_version = ">= 2.0", "< 5.0"
end
Download .txt
gitextract_4v6ma7m5/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── MIT-LICENSE.txt
├── README.md
├── Rakefile
├── data/
│   ├── cli-spinners.json
│   └── whirly-static-spinners.json
├── examples/
│   ├── all_spinners.rb
│   ├── asciinema_bundled_spinners.rb
│   ├── euruko.rb
│   ├── multi_lines.rb
│   ├── single.rb
│   └── status.rb
├── lib/
│   ├── whirly/
│   │   ├── spinners/
│   │   │   ├── cli.rb
│   │   │   └── whirly.rb
│   │   ├── spinners.rb
│   │   └── version.rb
│   └── whirly.rb
├── spec/
│   └── whirly_spec.rb
└── whirly.gemspec
Download .txt
SYMBOL INDEX (25 symbols across 6 files)

FILE: lib/whirly.rb
  type Whirly (line 11) | module Whirly
    function enabled? (line 45) | def enabled?
    function configured? (line 49) | def configured?
    function configure_spinner (line 55) | def self.configure_spinner(spinner_option)
    function configure_frames (line 85) | def self.configure_frames(spinner)
    function configure (line 114) | def self.configure(**options)
    function start (line 132) | def self.start(**options)
    function stop (line 180) | def self.stop(stop_frame = nil)
    function reset (line 194) | def self.reset
    function unrender (line 203) | def self.unrender
    function render (line 215) | def self.render(next_frame = nil)
    function render_prefix (line 225) | def self.render_prefix
    function render_suffix (line 233) | def self.render_suffix
    function initialize_color (line 240) | def self.initialize_color
    function next_color (line 249) | def self.next_color

FILE: lib/whirly/spinners.rb
  type Whirly (line 1) | module Whirly
    type Spinners (line 2) | module Spinners

FILE: lib/whirly/spinners/cli.rb
  type Whirly (line 3) | module Whirly
    type Spinners (line 4) | module Spinners

FILE: lib/whirly/spinners/whirly.rb
  type Whirly (line 3) | module Whirly
    type Spinners (line 4) | module Spinners

FILE: lib/whirly/version.rb
  type Whirly (line 3) | module Whirly

FILE: spec/whirly_spec.rb
  function short_sleep (line 7) | def short_sleep
  function medium_sleep (line 11) | def medium_sleep
  function long_sleep (line 15) | def long_sleep
Condensed preview — 24 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (83K chars).
[
  {
    "path": ".github/workflows/test.yml",
    "chars": 1181,
    "preview": "name: Test\n\non: [push, pull_request]\n\njobs:\n  test:\n    name: Ruby ${{ matrix.ruby }} (${{ matrix.os }})\n    if: \"!conta"
  },
  {
    "path": ".gitignore",
    "chars": 18,
    "preview": "Gemfile.lock\n/pkg\n"
  },
  {
    "path": ".gitmodules",
    "chars": 132,
    "preview": "[submodule \"data/external/cli-spinners\"]\n\tpath = data/external/cli-spinners\n\turl = https://github.com/sindresorhus/cli-s"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1769,
    "preview": "## CHANGELOG\n\n### 0.4.0\n\n- Allow Ruby 4.x\n- Add json (default) gem as dependency\n- Update CLI spinners to 3.3.0\n\n### 0.3"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3231,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "Gemfile",
    "chars": 93,
    "preview": "source 'https://rubygems.org'\n\ngemspec\n\ngem 'minitest'\ngem 'paint'\ngem 'rake'\ngem 'stringio'\n"
  },
  {
    "path": "MIT-LICENSE.txt",
    "chars": 1075,
    "preview": "Copyright (c) 2016 Jan Lelis, https://janlelis.com\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "README.md",
    "chars": 7950,
    "preview": "# Whirly 😀 [![[version]](https://badge.fury.io/rb/whirly.svg)](https://badge.fury.io/rb/whirly)  [<img src=\"https://gith"
  },
  {
    "path": "Rakefile",
    "chars": 1337,
    "preview": "# # #\n# Get gemspec info\n\ngemspec_file = Dir['*.gemspec'].first\ngemspec = eval File.read(gemspec_file), binding, gemspec"
  },
  {
    "path": "data/cli-spinners.json",
    "chars": 19423,
    "preview": "{\n\t\"dots\": {\n\t\t\"interval\": 80,\n\t\t\"frames\": [\n\t\t\t\"⠋\",\n\t\t\t\"⠙\",\n\t\t\t\"⠹\",\n\t\t\t\"⠸\",\n\t\t\t\"⠼\",\n\t\t\t\"⠴\",\n\t\t\t\"⠦\",\n\t\t\t\"⠧\",\n\t\t\t\"⠇\",\n\t\t\t"
  },
  {
    "path": "data/whirly-static-spinners.json",
    "chars": 3996,
    "preview": "{\n  \"roman_numerals\": {\n    \"interval\": 90,\n    \"mode\": \"swing\",\n    \"frames\": [\n      \"Ⅰ\",\n      \"Ⅱ\",\n      \"Ⅲ\",\n      "
  },
  {
    "path": "examples/all_spinners.rb",
    "chars": 449,
    "preview": "require_relative \"../lib/whirly\"\nrequire \"paint\"\n\n# Demonstrates all available spinners\n\nif spinner_pack = $*[0]\n  const"
  },
  {
    "path": "examples/asciinema_bundled_spinners.rb",
    "chars": 292,
    "preview": "require_relative \"../lib/whirly\"\nrequire \"paint\"\n\nsystem \"clear\"\n\nWhirly::Spinners::WHIRLY.keys.sort.each{ |spinner_name"
  },
  {
    "path": "examples/euruko.rb",
    "chars": 1114,
    "preview": "require_relative '../lib/whirly'\nrequire 'paint'\n\n# Lightning talk at EuRuKo 2016\n\n# # # Whirly\n\nprint \"\\033c\"\nputs Pain"
  },
  {
    "path": "examples/multi_lines.rb",
    "chars": 330,
    "preview": "require_relative \"../lib/whirly\"\nrequire \"paint\"\n\n# Demonstrate the look of using multiple spinners\n\nWhirly.configure(sp"
  },
  {
    "path": "examples/single.rb",
    "chars": 193,
    "preview": "require_relative \"../lib/whirly\"\nrequire \"paint\"\n\n# Call a single spinner from the command-line\n\nWhirly.start(spinner: $"
  },
  {
    "path": "examples/status.rb",
    "chars": 161,
    "preview": "require_relative \"../lib/whirly\"\n\nWhirly.start status: \"Initial status, passed when starting Whirly\"\nsleep 3\nWhirly.stat"
  },
  {
    "path": "lib/whirly/spinners/cli.rb",
    "chars": 157,
    "preview": "require \"json\"\n\nmodule Whirly\n  module Spinners\n    CLI = JSON.load(File.read(File.dirname(__FILE__) + \"/../../../data/c"
  },
  {
    "path": "lib/whirly/spinners/whirly.rb",
    "chars": 624,
    "preview": "require \"json\"\n\nmodule Whirly\n  module Spinners\n    WHIRLY = {\n      \"random_dots\"         => { \"proc\" => ->(){ [ 0x2800"
  },
  {
    "path": "lib/whirly/spinners.rb",
    "chars": 110,
    "preview": "module Whirly\n  module Spinners\n  end\nend\n\nrequire_relative \"spinners/whirly\"\nrequire_relative \"spinners/cli\"\n"
  },
  {
    "path": "lib/whirly/version.rb",
    "chars": 69,
    "preview": "# frozen_string_literal: true\n\nmodule Whirly\n  VERSION = \"0.4.0\"\nend\n"
  },
  {
    "path": "lib/whirly.rb",
    "chars": 6834,
    "preview": "require_relative \"whirly/version\"\nrequire_relative \"whirly/spinners\"\n\nrequire \"unicode/display_width\"\n\nbegin\n  require \""
  },
  {
    "path": "spec/whirly_spec.rb",
    "chars": 9802,
    "preview": "require_relative \"../lib/whirly\"\nrequire \"minitest/autorun\"\nrequire \"paint\"\n# require \"irbtools/binding\"\nrequire \"string"
  },
  {
    "path": "whirly.gemspec",
    "chars": 1082,
    "preview": "# -*- encoding: utf-8 -*-\n\nrequire File.dirname(__FILE__) + \"/lib/whirly/version\"\n\nGem::Specification.new do |gem|\n  gem"
  }
]

About this extraction

This page contains the full source code of the janlelis/whirly GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 24 files (60.0 KB), approximately 26.8k tokens, and a symbol index with 25 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!