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 ![](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 . 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