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) [
](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

### Bundled Whirly Spinners
[Play on asciinema](https://asciinema.org/a/88198?size=big)
### Bundled Spinners from CLI Spinners

[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 . Released under the MIT license.
- Contains data from cli-spinners: MIT License, Copyright (c) Sindre Sorhus (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