New Upstream Snapshot - ruby-tty-spinner

Ready changes

Summary

Merged new upstream version: 0.9.3+git20220802.1.278d946 (was: 0.9.3).

Resulting package

Built on 2023-01-16T22:22 (took 2m47s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-snapshots ruby-tty-spinner

Lintian Result

Diff

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..2bb2923
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*.rb]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..65122f0
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,3 @@
+--color
+--require spec_helper
+--warnings
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21c5469..5be3ab8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,139 +1,75 @@
 # Change log
 
-## [v0.9.3] - 2020-01-28
+## [v0.7.1] - 2020-01-25
 
 ### Changed
-* Change gemspec to add metadata, remove test artefacts and load version directly
+* Change gemspec to include metadata and remove test files
 
-## [v0.9.2] - 2019-12-08
-
-### Fixed
-* Fix multi spinner cursor hiding by @benklop
-
-## [v0.9.1] - 2019-05-29
-
-### Changed
-* Change bundler to remove version limit
-* Change to update tty-cursor dependency
-
-## [v0.9.0] - 2018-12-01
-
-### Changed
-* Change tty-cursor dependency
-* Change to Ruby >= 2.0
-* Change to freeze all string literals
-* Change #execute_job to stop evaluating in spinner context and just execute the job
-* Change #register to accept a spinner instance by Shane Cavanaugh(@shanecav84)
-
-### Fixed
-* Fix to remove a stray single quote in spin_4 by Kristofer Rye(@rye)
-* Fix Multi#line_inset to correctly assign styling in threaded environment
-* Fix #stop & #auto_spin to always restore hidden cursor if enabled
-* Fix deadlock when registering multi spinners
-
-## [v0.8.0] - 2018-01-11
-
-### Added
-* Add new formats :bounce, :burger, :dance, :dot_2, ..., dot_11, :shark, :pong
-
-### Changed
-* Change to only output to a console and stop output to a file, pipe etc...
-
-### Fixed
-* Fix spinner #stop to clear line before printing final message
-
-## [v0.7.0] - 2017-09-11
+## [v0.7.0] - 2019-05-27
 
 ### Added
-* Add :spin event type and emit from TTY::Spinner#spin
+* Add #scroll_up & #scroll_down display by one line
 
 ### Changed
-* Change to automatically spin top level multi spinner when registered spinners spin
-* Remove unnecessary checks for top spinner in multi spinner #stop, #success, #error
+* Change to restrict gem to Ruby >= 2.0
 
-### Fixed
-* Fix multi spinner #observe to only listen for events from registered spinners
-
-## [v0.6.0] - 2017-09-07
+## [v0.6.1] - 2019-02-28
 
 ### Changed
-* Change TTY::Spinner::Multi to render registered spinners at row
-  position at point of rendering and not registration
-
-### Fixed
-* Fix handling of multi spinner events
-* Fix multi spinner display for unicode inset characters
-
-## [v0.5.0] - 2017-08-09
+* Change gemspec to load files without git
 
-### Added
-* Add TTY::Spinner::Multi to allow for parallel spinners executation by Austin Blatt[@austb]
-* Add formatting for multi spinner display by Austin Blatt[@austb]
-* Add ability to add and execute jobs for single and multi spinners
-* Add abilty to register multi spinners with async jobs
-* Add #pause and #resume for single and multispinner
+## [v0.6.0] - 2018-07-13
 
 ### Changed
-* Change to unify success category to mark spinner as succeded or errored
-* Change Spinner to be thread safe
+* Remove encoding magic comments
 
 ### Fixed
-* Stop firing events when a spinner is stopped
+* Fix #clear_line_before/after to use correct control symbols by Xinyue Lu(@msg7086)
+* Fix gem vendoring by moving version file to its own folder
 
-## [v0.4.1] - 2016-08-07
+## [v0.5.0] - 2017-08-01
 
 ### Changed
-* Change #update to clear output when in spinning state
+* Change #save & #restore to work on major systems by Austin Blatt[@austb]
 
-## [v0.4.0] - 2016-08-07
+## [v0.4.0] - 2017-01-08
 
 ### Added
-* Add #auto_spin to automatically displaying spinning animation
+* Add #clear_char for erasing characters
+* Add #clear_line_before for erasing line before the cursor
+* Add #clear_line_after for erasing line after the cursor
+* Add #column to move the cursor horizontally in the current line
+* Add #row to move the cursor vertically in the current column
 
 ### Changed
-* Change #start to setup timer and reset done state
+* Remove #move_start
+* Change #next_line to move the cursor to beginning of the line
+* Change #clear_line to move the cursor to beginning of the line
+* Change alias_method to alias
 
-## [v0.3.0] - 2016-07-14
+### Fixed
+* Fix #clear_line to correctly clear whole line
 
-### Added
-* Add #run to automatically execute job with spinning animation by @Thermatix
-* Add #update to allow for dynamic label name replacement
+## [v0.3.0] - 2016-05-21
 
 ### Fixed
-* Fixed cursor hiding for success and error calls by @m-o-e
-* Fix #join call to define actual error
-* Fix #stop to print only once when finished
-
-## [v0.2.0] - 2016-03-13
+* Fix prev_line to work in iTerm2 and Putty by @m-o-e
 
-### Added
-* Add new spinner formats by @rlqualls
-* Add ability to specify custom frames through :frames option
-* Add :clear option for removing spinner output when done
-* Add #success and #error calls for stopping spinner
-* Add :done, :success, :error completion events
-* Add :success_mark & :error_mark to allow changing markers
-* Add :interval for automatic spinning duration
-* Add #start, #join and #kill for automatic spinner animation
+## [v0.2.0] - 2015-12-28
 
 ### Changed
-* Change message formatting, use :spinner token to customize message
-* Change format for defining spinner formats and intervals
+* Change #clear_lines to clear first and move up/down
 
-## [v0.1.0] - 2014-11-15
+## [v0.1.0] - 2015-11-28
 
 * Initial implementation and release
 
-[v0.9.3]: https://github.com/piotrmurach/tty-spinner/compare/v0.9.2...v0.9.3
-[v0.9.2]: https://github.com/piotrmurach/tty-spinner/compare/v0.9.1...v0.9.2
-[v0.9.1]: https://github.com/piotrmurach/tty-spinner/compare/v0.9.0...v0.9.1
-[v0.9.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.8.0...v0.9.0
-[v0.8.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.7.0...v0.8.0
-[v0.7.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.6.0...v0.7.0
-[v0.6.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.5.0...v0.6.0
-[v0.5.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.4.1...v0.5.0
-[v0.4.1]: https://github.com/piotrmurach/tty-spinner/compare/v0.4.0...v0.4.1
-[v0.4.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.3.0...v0.4.0
-[v0.3.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.2.0...v0.3.0
-[v0.2.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.1.0...v0.2.0
-[v0.1.0]: https://github.com/piotrmurach/tty-spinner/compare/v0.1.0
+[v0.7.1]: https://github.com/piotrmurach/tty-cursor/compare/v0.7.0...v0.7.1
+[v0.7.0]: https://github.com/piotrmurach/tty-cursor/compare/v0.6.1...v0.7.0
+[v0.6.1]: https://github.com/piotrmurach/tty-cursor/compare/v0.6.0...v0.6.1
+[v0.6.0]: https://github.com/piotrmurach/tty-cursor/compare/v0.5.0...v0.6.0
+[v0.5.0]: https://github.com/piotrmurach/tty-cursor/compare/v0.4.0...v0.5.0
+[v0.4.0]: https://github.com/piotrmurach/tty-cursor/compare/v0.3.0...v0.4.0
+[v0.3.0]: https://github.com/piotrmurach/tty-cursor/compare/v0.2.0...v0.3.0
+[v0.2.0]: https://github.com/piotrmurach/tty-cursor/compare/v0.1.0...v0.2.0
+[v0.1.0]: https://github.com/piotrmurach/tty-cursor/compare/101e78d...v0.1.0
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..a6e4f9f
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,132 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual
+identity and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall
+  community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or advances of
+  any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address,
+  without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders 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, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+piotr@piotrmurach.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series of
+actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or permanent
+ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within the
+community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
+[https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..aafc0aa
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,13 @@
+source "https://rubygems.org"
+
+gemspec
+
+gem "json", "2.4.1" if RUBY_VERSION == "2.0.0"
+
+group :metrics do
+  if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5.0")
+    gem "coveralls_reborn", "~> 0.21.0"
+    gem "simplecov", "~> 0.21.0"
+  end
+  gem "yardstick", "~> 0.9.9"
+end
diff --git a/LICENSE.txt b/LICENSE.txt
index 3a6b46b..443ce8e 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2014 Piotr Murach
+Copyright (c) 2015 Piotr Murach (piotrmurach.com)
 
 MIT License
 
diff --git a/README.md b/README.md
index d81d873..2f3c8df 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,36 @@
 <div align="center">
-  <a href="https://piotrmurach.github.io/tty" target="_blank"><img width="130" src="https://github.com/piotrmurach/tty/raw/master/images/tty.png" alt="tty logo" /></a>
+  <a href="https://ttytoolkit.org"><img width="130" src="https://github.com/piotrmurach/tty/raw/master/images/tty.png" alt="TTY Toolkit logo" /></a>
 </div>
 
-# TTY::Spinner [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
+# TTY::Cursor [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter]
 
-[![Gem Version](https://badge.fury.io/rb/tty-spinner.svg)][gem]
-[![Build Status](https://secure.travis-ci.org/piotrmurach/tty-spinner.svg?branch=master)][travis]
-[![Build status](https://ci.appveyor.com/api/projects/status/2i5lx3tvyi5l8x3j?svg=true)][appveyor]
-[![Maintainability](https://api.codeclimate.com/v1/badges/d5ae2219e194ac99be58/maintainability)][codeclimate]
-[![Coverage Status](https://coveralls.io/repos/piotrmurach/tty-spinner/badge.svg)][coverage]
-[![Inline docs](http://inch-ci.org/github/piotrmurach/tty-spinner.svg?branch=master)][inchpages]
+[![Gem Version](https://badge.fury.io/rb/tty-cursor.svg)][gem]
+[![Actions CI](https://github.com/piotrmurach/tty-cursor/workflows/CI/badge.svg?branch=master)][gh_actions_ci]
+[![Build status](https://ci.appveyor.com/api/projects/status/4k7cd69jscwg7fl7?svg=true)][appveyor]
+[![Code Climate](https://codeclimate.com/github/piotrmurach/tty-cursor/badges/gpa.svg)][codeclimate]
+[![Coverage Status](https://coveralls.io/repos/piotrmurach/tty-cursor/badge.svg)][coverage]
+[![Inline docs](http://inch-ci.org/github/piotrmurach/tty-cursor.svg?branch=master)][inchpages]
 
 [gitter]: https://gitter.im/piotrmurach/tty
-[gem]: http://badge.fury.io/rb/tty-spinner
-[travis]: http://travis-ci.org/piotrmurach/tty-spinner
-[appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-spinner
-[codeclimate]: https://codeclimate.com/github/piotrmurach/tty-spinner/maintainability
-[coverage]: https://coveralls.io/r/piotrmurach/tty-spinner
-[inchpages]: http://inch-ci.org/github/piotrmurach/tty-spinner
+[gem]: http://badge.fury.io/rb/tty-cursor
+[gh_actions_ci]: https://github.com/piotrmurach/tty-cursor/actions?query=workflow%3ACI
+[appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-cursor
+[codeclimate]: https://codeclimate.com/github/piotrmurach/tty-cursor
+[coverage]: https://coveralls.io/r/piotrmurach/tty-cursor
+[inchpages]: http://inch-ci.org/github/piotrmurach/tty-cursor
 
-> A terminal spinner for tasks that have non-deterministic time frame.
+> Terminal cursor positioning, visibility and text manipulation.
 
-**TTY::Spinner** provides independent spinner component for [TTY](https://github.com/piotrmurach/tty) toolkit.
+The purpose of this library is to help move the terminal cursor around and manipulate text by using intuitive method calls.
 
-![](demo.gif)
+**TTY::Cursor** provides independent cursor movement component for [TTY](https://github.com/piotrmurach/tty) toolkit.
 
 ## Installation
 
 Add this line to your application's Gemfile:
 
 ```ruby
-gem 'tty-spinner'
+gem 'tty-cursor'
 ```
 
 And then execute:
@@ -39,521 +39,226 @@ And then execute:
 
 Or install it yourself as:
 
-    $ gem install tty-spinner
+    $ gem install tty-cursor
 
 ## Contents
 
 * [1. Usage](#1-usage)
-* [2. TTY::Spinner API](#2-ttyspinner-api)
-  * [2.1 spin](#21-spin)
-  * [2.2 auto_spin](#22-auto_spin)
-    * [2.2.1 pause](#221-pause)
-    * [2.2.2 resume](#222-resume)
-  * [2.3 run](#23-run)
-  * [2.4 start](#24-start)
-  * [2.5 stop](#25-stop)
-    * [2.5.1 success](#251-success)
-    * [2.5.2 error](#252-error)
-  * [2.6 update](#26-update)
-  * [2.7 reset](#27-reset)
-  * [2.8 join](#28-join)
-* [3. Configuration](#3-configuration)
-  * [3.1 :format](#31-format)
-  * [3.2 :frames](#32-frames)
-  * [3.3 :interval](#33-interval)
-  * [3.4 :hide_cursor](#34-hide_cursor)
-  * [3.5 :clear](#35-clear)
-  * [3.6 :success_mark](#36-success_mark)
-  * [3.7 :error_mark](#37-error_mark)
-  * [3.8 :output](#38-output)
-* [4. Events](#4-events)
-  * [4.1 done](#41-done)
-  * [4.2 success](#42-success)
-  * [4.3 error](#43-error)
-* [5. TTY::Spinner::Multi API](#5-ttyspinnermulti-api)
-  * [5.1 register](#51-register)
-  * [5.2 auto_spin](#52-auto_spin)
-    * [5.2.1 manual async](#521-manual-async)
-    * [5.2.2 auto async tasks](#522-auto-async-tasks)
-  * [5.3 stop](#53-stop)
-    * [5.3.1 success](#531-success)
-    * [5.3.2 error](#532-error)
-  * [5.4 :style](#54-style)
+* [2. Interface](#2-interface)
+  * [2.1 Cursor Positioning](#21-cursor-positioning)
+    * [2.1.1 move_to(x, y)](#211-move_tox-y)
+    * [2.1.2 move(x, y)](#212-movex-y)
+    * [2.1.3 up(n)](#213-upn)
+    * [2.1.4 down(n)](#214-downn)
+    * [2.1.5 forward(n)](#215-forwardn)
+    * [2.1.6 backward(n)](#216-backwardn)
+    * [2.1.7 column(n)](#217-columnn)
+    * [2.1.8 row(n)](#218-rown)
+    * [2.1.9 next_line](#219-next_line)
+    * [2.1.10 prev_line](#2110-prev_line)
+    * [2.1.11 save](#2111-save)
+    * [2.1.12 restore](#2112-restore)
+    * [2.1.13 current](#2113-current)
+  * [2.2 Cursor Visibility](#22-cursor-visibility)
+    * [2.2.1 show](#221-show)
+    * [2.2.2 hide](#222-hide)
+    * [2.2.3 invisible(stream)](#223-invisiblestream)
+  * [2.3 Text Clearing](#23-text-clearing)
+    * [2.3.1 clear_char(n)](#231-clear_charn)
+    * [2.3.2 clear_line](#232-clear_line)
+    * [2.3.3 clear_line_before](#233-clear_line_before)
+    * [2.3.4 clear_line_after](#234-clear_line_after)
+    * [2.3.5 clear_lines(n, direction)](#235-clear_linesn-direction)
+    * [2.3.6 clear_screen_down](#236-clear_screen_down)
+    * [2.3.7 clear_screen_up](#237-clear_screen_up)
+    * [2.3.8 clear_screen](#238-clear_screen)
+  * [2.4 Scrolling](#24-scrolling)
+    * [2.4.1 scroll_down](#241-scroll_down)
+    * [2.4.2 scroll_up](#242-scroll_up)
 
 ## 1. Usage
 
-**TTY::Spinner** by default uses `:classic` type of formatter and requires no parameters:
+**TTY::Cursor** is just a module hence you can reference it for later like so:
 
 ```ruby
-spinner = TTY::Spinner.new
+cursor = TTY::Cursor
 ```
 
-In addition you can provide a message with `:spinner` token and format type you would like for the spinning display:
+and to move the cursor current position by 5 rows up and 2 columns right do:
 
 ```ruby
-spinner = TTY::Spinner.new("[:spinner] Loading ...", format: :pulse_2)
-
-spinner.auto_spin # Automatic animation with default interval
-
-sleep(2) # Perform task
-
-spinner.stop('Done!') # Stop animation
-```
-
-This would produce animation in your terminal:
-
-```ruby
-⎺ Loading ...
-```
-
-and when finished output:
-
-```ruby
-_ Loading ... Done!
-```
-
-Use **TTY::Spinner::Multi** to synchronize multiple spinners:
-
-```ruby
-spinners = TTY::Spinner::Multi.new("[:spinner] top")
-
-sp1 = spinners.register "[:spinner] one"
-# or sp1 = ::TTY::Spinner.new("[:spinner] one")
-sp2 = spinners.register "[:spinner] two"
-
-sp1.auto_spin
-sp2.auto_spin
-
-sleep(2) # Perform work
-
-sp1.success
-sp2.success
-```
-
-which when done will display:
-
-```ruby
-┌ [✔] top
-├── [✔] one
-└── [✔] two
-```
-
-For more usage examples please see [examples directory](https://github.com/piotrmurach/tty-spinner/tree/master/examples)
-
-## 2. TTY::Spinner API
-
-### 2.1 spin
-
-The main workhorse of the spinner is the `spin` method.
-
-Looping over `spin` method will animate a given spinner.
-
-```ruby
-loop do
-  spinner.spin
-end
-```
-
-### 2.2 auto_spin
-
-To perform automatic spinning animation use `auto_spin` method like so:
-
-```ruby
-spinner.auto_spin
-```
-
-The speed with which the spinning happens is determined by the `:interval` parameter. All the spinner formats have their default intervals specified ([see](https://github.com/piotrmurach/tty-spinner/blob/master/lib/tty/spinner/formats.rb)).
-
-### 2.2.1 pause
-
-After calling `auto_spin` you can pause spinner execution:
-
-```ruby
-spinner.pause
-```
-
-### 2.2.2 resume
-
-You can continue any paused spinner:
-
-```ruby
-spinner.resume
+print cursor.up(5) + cursor.forward(2)
 ```
 
-### 2.3 run
-
-Use `run` passing a block with a job that will automatically display spinning animation while the block executes and finish animation when the block terminates. The block yields a spinner instance.
+or call `move` to move cursor relative to current position:
 
 ```ruby
-spinner.run do |spinner|
-  ...
-end
+print cursor.move(5, 2)
 ```
 
-Optionally you can provide a stop message to display when animation is finished.
+to remove text from the current line do:
 
 ```ruby
-spinner.run('Done!') do |spinner|
-  ...
-end
+print cursor.clear_line
 ```
 
-### 2.4 start
+## 2. Interface
 
-In order to set start time or reuse the same spinner after it has stopped, call `start` method:
+### 2.1 Cursor Positioning
 
-```ruby
-spinner.start
-```
+All methods in this section allow to position the cursor around the terminal viewport.
 
-### 2.5 stop
+Cursor movement will be bounded by the current viewport into the buffer. Scrolling (if available) will not occur.
 
-In order to stop the spinner call `stop`. This will finish drawing the spinning animation and return to new line.
+#### 2.1.1 move_to(x, y)
 
-```ruby
-spinner.stop
-```
+Set the cursor absolute position to `x` and `y` coordinate, where `x` is the column of the `y` line.
 
-You can further pass a message to print when animation is finished.
+If no row/column parameters are provided, the cursor will move to the home position, at the upper left of the screen:
 
 ```ruby
-spinner.stop('Done!')
+cursor.move_to
 ```
 
-#### 2.5.1 success
+#### 2.1.2 move(x, y)
 
-Use `success` call to stop the spinning animation and replace the spinning symbol with check mark character to indicate successful completion.
+Move cursor by x columns and y rows relative to its current position.
 
-```ruby
-spinner = TTY::Spinner.new("[:spinner] Task name")
-spinner.success('(successful)')
-```
+#### 2.1.3 up(n)
 
-This will produce:
+Move the cursor up by `n` rows; the default n is `1`.
 
-```
-[✔] Task name (successful)
-```
+#### 2.1.4 down(n)
 
-#### 2.5.2 error
+Move the cursor down by `n` rows; the default n is `1`.
 
-Use `error` call to stop the spinning animation and replace the spinning symbol with cross character to indicate error completion.
+#### 2.1.5 forward(n)
 
-```ruby
-spinner = TTY::Spinner.new("[:spinner] Task name")
-spinner.error('(error)')
-```
+Move the cursor forward by `n` columns; the default n is `1`.
 
-This will produce:
+#### 2.1.6 backward(n)
 
-```ruby
-[✖] Task name (error)
-```
+Move the cursor backward by `n` columns; the default n is `1`.
 
-### 2.6 update
+#### 2.1.7 column(n)
 
-Use `update` call to dynamically change label name(s).
+Cursor moves to `<n>`th position horizontally in the current line.
 
-Provide an arbitrary token name(s) in the message string, such as `:title`
+#### 2.1.8 row(n)
 
-```ruby
-spinner = TTY::Spinner.new("[:spinner] :title")
-```
+Cursor moves to the `<n>`th position vertically in the current column.
 
-and then pass token name and value:
+#### 2.1.9 next_line
 
-```ruby
-spinner.update(title: 'Downloading file1')
-```
+Move the cursor down to the beginning of the next line.
 
-next start animation:
+#### 2.1.10 prev_line
 
-```ruby
-spinner.run { ... }
-# => | Downloading file1
-```
+Move the cursor up to the beginning of the previous line.
 
-Once animation finishes you can kick start another one with a different name:
+#### 2.1.11 save
 
-```ruby
-spinner.update(title: 'Downloading file2')
-spinner.run { ... }
-```
+Save current cursor position.
 
-### 2.7 reset
+#### 2.1.12 restore
 
-In order to reset the spinner to its initial frame do:
+Restore cursor position after a save cursor was called.
 
-```ruby
-spinner.reset
-```
+#### 2.1.13 current
 
-### 2.8 join
+Query current cursor position
 
-One way to wait while the spinning animates is to join the thread started with `start` method:
+### 2.2 Cursor Visibility
 
-```ruby
-spinner.join
-```
+The following methods control the visibility of the cursor.
 
-Optionally you can provide timeout:
+#### 2.2.1 show
 
-```ruby
-spinner.join(0.5)
-```
+Show the cursor.
 
-## 3. Configuration
+#### 2.2.2 hide
 
-There are number of configuration options that can be provided to customise the behaviour of a spinner.
+Hide the cursor.
 
-### 3.1 :format
+#### 2.2.3 invisible(stream)
 
-Use one of the predefined spinner styles by passing the formatting token `:format`
+To hide the cursor for the duration of the block do:
 
 ```ruby
-spinner = TTY::Spinner.new(format: :pulse_2)
+cursor.invisible { ... }
 ```
 
-All spinner formats that **TTY::Spinner** accepts are defined in [/lib/tty/spinner/formats.rb](https://github.com/piotrmurach/tty-spinner/blob/master/lib/tty/spinner/formats.rb)
-
-If you wish to see all available formats in action run the `formats.rb` file in examples folder like so:
+By default standard output will be used but you can change that by passing a different stream that responds to `print` call:
 
 ```ruby
-bundle exec ruby examples/formats.rb
+cursor.invisible($stderr) { .... }
 ```
 
-### 3.2 :frames
-
-If you wish to use custom formatting use the `:frames` option with either `array` or `string` of characters.
-
-```ruby
-spinner = TTY::Spinner.new(frames: [".", "o", "0", "@", "*"])
-```
+### 2.3 Text Clearing
 
-### 3.3 :interval
+All methods in this section provide APIs to modify text buffer contents.
 
-The `:interval` option  accepts `integer` representing number of `Hz` units, for instance, frequency of 10 will mean that the spinning animation will be displayed 10 times per second.
+#### 2.3.1 clear_char(n)
 
-```ruby
-spinner = TTY::Spinner.new(interval: 20) # 20 Hz (20 times per second)
-```
+Erase `<n>` characters from the current cursor position by overwriting them with space character.
 
-### 3.4 :hide_cursor
+#### 2.3.2 clear_line
 
-Hides cursor when spinning animation performs. Defaults to `false`.
+Erase the entire current line and return cursor to beginning of the line.
 
-```ruby
-spinner = TTY::Spinner.new(hide_cursor: true)
-```
+#### 2.3.3 clear_line_before
 
-### 3.5 :clear
+Erase from the beginning of the line up to and including the current position.
 
-After spinner is finished clears its output. Defaults to `false`.
+#### 2.3.4 clear_line_after
 
-```ruby
-spinner = TTY::Spinner.new(clear: true)
-```
+Erase from the current position (inclusive) to the end of the line/display.
 
-### 3.6 :success_mark
+#### 2.3.5 clear_lines(n, direction)
 
-To change marker indicating successful completion use the `:success_mark` option:
+Erase `n` rows in given direction; the default direction is `:up`.
 
 ```ruby
-spinner = TTY::Spinner.new(success_mark: '+')
+cursor.clear_lines(5, :down)
 ```
 
-### 3.7 :error_mark
+#### 2.3.6 clear_screen
 
-To change marker indicating error completion use the `:error_mark` option:
+Erase the screen with the background colour and moves the cursor to home.
 
-```ruby
-spinner = TTY::Spinner.new(error_mark: 'x')
-```
+#### 2.3.7 clear_screen_down
 
-### 3.8 :output
+Erase the screen from the current line down to the bottom of the screen.
 
-The spinner only outputs to a console and when output is redirected to a file or a pipe it does nothing. This is so, for example, your error logs do not overflow with spinner output.
+#### 2.3.8 clear_screen_up
 
-You can change where console output is streamed with `:output` option:
+Erase the screen from the current line up to the top of the screen.
 
-```ruby
-spinner = TTY::Spinner.new(output: $stdout)
-```
+### 2.4 Scrolling
 
-The output stream defaults to `stderr`.
+#### 2.4.1 scroll_down
 
-## 4. Events
+Scroll display down one line.
 
-**TTY::Spinner** emits `:done`, `:success` and `:error` event types when spinner is stopped.
+### 2.4.2 scroll_up
 
-### 4.1 done
+Scroll display up one line.
 
-This event is emitted irrespective of the completion method. In order to listen for this event you need to register callback:
-
-```ruby
-spinner.on(:done) { ... }
-```
-
-### 4.2 success
-
-This event is fired when `success` call is made. In order to respond to the event, you need to register callback:
-
-```ruby
-spinner.on(:success) { ... }
-```
-
-### 4.3 error
-
-This event is fired when `error` completion is called. In order to respond to the event, you need to register callback:
-
-```ruby
-spinner.on(:error) { ... }
-```
-
-## 5. TTY::Spinner::Multi API
-
-### 5.1 register
-
-Create and register a `TTY::Spinner` under the multispinner
-
-```ruby
-new_spinner = multi_spinner.register("[:spinner] Task 1 name", options)
-# or
-#   spinner = ::TTY::Spinner.new("[:spinner] one")
-#   sp1 = multi_spinner.register(spinner)
-```
-
-If no options are given it will use the options given to the multi_spinner when it was initialized to create the new spinner.
-If options are passed, they will override any options given to the multi spinner.
-
-### 5.2 auto_spin
-
-To create a top level spinner that tracks activity of all the registered spinners, the multispinner has to have been given a message on initialization:
-
-```ruby
-multi_spinner = TTY::Spinner::Multi.new("[:spinner] Top level spinner")
-```
-
-The top level multi spinner will perform spinning animation automatically when at least one of the registered spinners starts spinning.
-
-If you register spinners without any tasks then you will have to manually control when the `multi_spinner` finishes by calling `stop`, `success` or `error` (see [manual](#521-manual-async)).
-
-Alternatively, you can register spinners with tasks that will automatically animate and finish spinners when respective tasks are done (see [async tasks](#522-auto-async-tasks)).
-
-The speed with which the spinning happens is determined by the `:interval` parameter. All the spinner formats have their default intervals specified ([see](https://github.com/piotrmurach/tty-spinner/blob/master/lib/tty/spinner/formats.rb)).
-
-#### 5.2.1 manual async
-
-In case when you wish to have full control over multiple spinners, you will need to perform all actions manually.
-
-For example, create a multi spinner that will track status of all registered spinners:
-
-```ruby
-multi_spinner = TTY::Spinner::Multi.new("[:spinner] top")
-```
-
-and then register spinners with their formats:
-
-```
-spinner_1 = spinners.register "[:spinner] one"
-spinner_2 = spinners.register "[:spinner] two"
-```
-
-Once registered, you can set spinners running in separate threads:
-
-```ruby
-spinner_1.auto_spin
-spinner_2.auto_spin
-```
-
-Finally, you need to stop each spinner manually, in our case we mark the second spinner as failure which in turn will stop the top level multi spinner automatically and mark it as failure:
-
-```ruby
-spinner_1.success
-spinner_2.error
-```
-
-The result may look like this:
-
-```ruby
-┌ [✖] top
-├── [✔] one
-└── [✖] two
-```
-
-#### 5.2.2 auto async tasks
-
-In case when you wish to execute async tasks and update individual spinners automatically, in any order, about their task status use `#register` and pass additional block parameter with the job to be executed.
-
-For example, create a multi spinner that will track status of all registered spinners:
-
-```ruby
-multi_spinner = TTY::Spinner::Multi.new("[:spinner] top")
-```
-
-and then register spinners with their respective tasks:
-
-```ruby
-multi_spinner.register("[:spinner] one") { |sp| sleep(2); sp.success('yes 2') }
-multi_spinner.register("[:spinner] two") { |sp| sleep(3); sp.error('no 2') }
-```
-
-Finally, call `#auto_spin` to kick things off:
-
-```ruby
-multi_spinner.auto_spin
-```
-
-If any of the child spinner stops with error then the top level spinner will be marked as failure.
-
-### 5.3 stop
-
-In order to stop the multi spinner call `stop`. This will stop the top level spinner, if it exists, and any sub-spinners still spinning.
-
-```ruby
-multi_spinner.stop
-```
-
-#### 5.3.1 success
-
-Use `success` call to stop the spinning animation and replace the spinning symbol with a check mark character to indicate successful completion.
-This will also call `#success` on any sub-spinners that are still spinning.
-
-```ruby
-multi_spinner.success
-```
-
-#### 5.3.2 error
-
-Use `error` call to stop the spinning animation and replace the spinning symbol with cross character to indicate error completion.
-This will also call `#error` on any sub-spinners that are still spinning.
-
-```ruby
-multi_spinner.error
-```
-
-### 5.4 :style
-
-In addition to all [configuration options](#3-configuration) you can style multi spinner like so:
-
-```ruby
-multi_spinner = TTY::Spinner::Multi.new("[:spinner] parent", style: {
-  top: '. '
-  middle: '|-> '
-  bottom: '|__ '
-})
-```
 
 ## Contributing
 
-Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/tty-spinner. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
-
-1. Fork it ( https://github.com/piotrmurach/tty-spinner/fork )
+1. Fork it ( https://github.com/piotrmurach/tty-cursor/fork )
 2. Create your feature branch (`git checkout -b my-new-feature`)
 3. Commit your changes (`git commit -am 'Add some feature'`)
 4. Push to the branch (`git push origin my-new-feature`)
 5. Create a new Pull Request
 
+This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
+
+## Code of Conduct
+
+Everyone interacting in the Strings::Inflection project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/tty-cursor/blob/master/CODE_OF_CONDUCT.md).
+
 ## Copyright
 
-Copyright (c) 2014 Piotr Murach. See LICENSE for further details.
+Copyright (c) 2015 Piotr Murach. See LICENSE for further details.
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..1828782
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,8 @@
+# encoding: utf-8
+
+require 'bundler/gem_tasks'
+
+FileList['tasks/**/*.rake'].each(&method(:import))
+
+desc 'Run all specs'
+task ci: %w[ spec ]
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..8184a8e
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,32 @@
+---
+skip_commits:
+  files:
+    - "bin/**"
+    - "*.md"
+install:
+  - SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
+  - gem install bundler -v '< 2.0'
+  - bundle install
+before_test:
+  - ruby -v
+  - gem -v
+  - bundle -v
+build: off
+test_script:
+  - bundle exec rake ci
+environment:
+  matrix:
+    - ruby_version: "200"
+    - ruby_version: "200-x64"
+    - ruby_version: "21"
+    - ruby_version: "21-x64"
+    - ruby_version: "22"
+    - ruby_version: "22-x64"
+    - ruby_version: "23"
+    - ruby_version: "23-x64"
+    - ruby_version: "24"
+    - ruby_version: "24-x64"
+    - ruby_version: "25"
+    - ruby_version: "25-x64"
+    - ruby_version: "26"
+    - ruby_version: "26-x64"
diff --git a/bin/console b/bin/console
new file mode 100755
index 0000000..5c61b80
--- /dev/null
+++ b/bin/console
@@ -0,0 +1,14 @@
+#!/usr/bin/env ruby
+
+require "bundler/setup"
+require "tty/cursor"
+
+# You can add fixtures and/or initialization code here to make experimenting
+# with your gem easier. You can also use a different console, if you like.
+
+# (If you use this, don't forget to add pry to your Gemfile!)
+# require "pry"
+# Pry.start
+
+require "irb"
+IRB.start(__FILE__)
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..dce67d8
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+set -euo pipefail
+IFS=$'\n\t'
+set -vx
+
+bundle install
+
+# Do any other automated setup that you need to do here
diff --git a/debian/changelog b/debian/changelog
index 9f0224c..2ea30bf 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,12 +1,13 @@
-ruby-tty-spinner (0.9.3-3) UNRELEASED; urgency=low
+ruby-tty-spinner (0.9.3+git20220802.1.278d946-1) UNRELEASED; urgency=low
 
   * Set field Upstream-Contact in debian/copyright.
   * Remove obsolete fields Contact, Name from debian/upstream/metadata (already
     present in machine-readable debian/copyright).
   * Bump debhelper from old 12 to 13.
   * Update standards version to 4.5.1, no changes needed.
+  * New upstream snapshot.
 
- -- Debian Janitor <janitor@jelmer.uk>  Sun, 30 Aug 2020 09:03:43 -0000
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 16 Jan 2023 22:21:03 -0000
 
 ruby-tty-spinner (0.9.3-2) unstable; urgency=medium
 
diff --git a/lib/tty-cursor.rb b/lib/tty-cursor.rb
new file mode 100644
index 0000000..4bca4ea
--- /dev/null
+++ b/lib/tty-cursor.rb
@@ -0,0 +1 @@
+require_relative 'tty/cursor'
diff --git a/lib/tty-spinner.rb b/lib/tty-spinner.rb
deleted file mode 100644
index 18079bb..0000000
--- a/lib/tty-spinner.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-require_relative 'tty/spinner'
-require_relative 'tty/spinner/multi'
diff --git a/lib/tty/cursor.rb b/lib/tty/cursor.rb
new file mode 100644
index 0000000..93ca504
--- /dev/null
+++ b/lib/tty/cursor.rb
@@ -0,0 +1,207 @@
+# frozen_string_literal: true
+
+require_relative 'cursor/version'
+
+module TTY
+  # Terminal cursor movement ANSI codes
+  module Cursor
+    module_function
+
+    ESC = "\e".freeze
+    CSI = "\e[".freeze
+    DEC_RST  = 'l'.freeze
+    DEC_SET  = 'h'.freeze
+    DEC_TCEM = '?25'.freeze
+
+    # Make cursor visible
+    # @api public
+    def show
+      CSI + DEC_TCEM + DEC_SET
+    end
+
+    # Hide cursor
+    # @api public
+    def hide
+      CSI + DEC_TCEM + DEC_RST
+    end
+
+    # Switch off cursor for the block
+    # @api public
+    def invisible(stream = $stdout)
+      stream.print(hide)
+      yield
+    ensure
+      stream.print(show)
+    end
+
+    # Save current position
+    # @api public
+    def save
+      Gem.win_platform? ? CSI + 's' : ESC + '7'
+    end
+
+    # Restore cursor position
+    # @api public
+    def restore
+      Gem.win_platform? ? CSI + 'u' : ESC + '8'
+    end
+
+    # Query cursor current position
+    # @api public
+    def current
+      CSI + '6n'
+    end
+
+    # Set the cursor absolute position
+    # @param [Integer] row
+    # @param [Integer] column
+    # @api public
+    def move_to(row = nil, column = nil)
+      return CSI + 'H' if row.nil? && column.nil?
+      CSI + "#{column + 1};#{row + 1}H"
+    end
+
+    # Move cursor relative to its current position
+    #
+    # @param [Integer] x
+    # @param [Integer] y
+    #
+    # @api public
+    def move(x, y)
+      (x < 0 ? backward(-x) : (x > 0 ? forward(x) : '')) +
+      (y < 0 ? down(-y) : (y > 0 ? up(y) : ''))
+    end
+
+    # Move cursor up by n
+    # @param [Integer] n
+    # @api public
+    def up(n = nil)
+      CSI + "#{(n || 1)}A"
+    end
+    alias cursor_up up
+
+    # Move the cursor down by n
+    # @param [Integer] n
+    # @api public
+    def down(n = nil)
+      CSI + "#{(n || 1)}B"
+    end
+    alias cursor_down down
+
+    # Move the cursor backward by n
+    # @param [Integer] n
+    # @api public
+    def backward(n = nil)
+      CSI + "#{n || 1}D"
+    end
+    alias cursor_backward backward
+
+    # Move the cursor forward by n
+    # @param [Integer] n
+    # @api public
+    def forward(n = nil)
+      CSI + "#{n || 1}C"
+    end
+    alias cursor_forward forward
+
+    # Cursor moves to nth position horizontally in the current line
+    # @param [Integer] n
+    #   the nth aboslute position in line
+    # @api public
+    def column(n = nil)
+      CSI + "#{n || 1}G"
+    end
+
+    # Cursor moves to the nth position vertically in the current column
+    # @param [Integer] n
+    #   the nth absolute position in column
+    # @api public
+    def row(n = nil)
+      CSI + "#{n || 1}d"
+    end
+
+    # Move cursor down to beginning of next line
+    # @api public
+    def next_line
+      CSI + 'E' + column(1)
+    end
+
+    # Move cursor up to beginning of previous line
+    # @api public
+    def prev_line
+      CSI + 'A' + column(1)
+    end
+
+    # Erase n characters from the current cursor position
+    # @api public
+    def clear_char(n = nil)
+      CSI + "#{n}X"
+    end
+
+    # Erase the entire current line and return to beginning of the line
+    # @api public
+    def clear_line
+      CSI + '2K' + column(1)
+    end
+
+    # Erase from the beginning of the line up to and including
+    # the current cursor position.
+    # @api public
+    def clear_line_before
+      CSI + '1K'
+    end
+
+    # Erase from the current position (inclusive) to
+    # the end of the line
+    # @api public
+    def clear_line_after
+      CSI + '0K'
+    end
+
+    # Clear a number of lines
+    #
+    # @param [Integer] n
+    #   the number of lines to clear
+    # @param [Symbol] :direction
+    #   the direction to clear, default :up
+    #
+    # @api public
+    def clear_lines(n, direction = :up)
+      n.times.reduce([]) do |acc, i|
+        dir = direction == :up ? up : down
+        acc << clear_line + ((i == n - 1) ? '' : dir)
+      end.join
+    end
+    alias clear_rows clear_lines
+
+    # Clear screen down from current position
+    # @api public
+    def clear_screen_down
+      CSI + 'J'
+    end
+
+    # Clear screen up from current position
+    # @api public
+    def clear_screen_up
+      CSI + '1J'
+    end
+
+    # Clear the screen with the background colour and moves the cursor to home
+    # @api public
+    def clear_screen
+      CSI + '2J'
+    end
+
+    # Scroll display up one line
+    # @api public
+    def scroll_up
+      ESC + 'M'
+    end
+
+    # Scroll display down one line
+    # @api public
+    def scroll_down
+      ESC + 'D'
+    end
+  end # Cursor
+end # TTY
diff --git a/lib/tty/cursor/version.rb b/lib/tty/cursor/version.rb
new file mode 100644
index 0000000..f18f47c
--- /dev/null
+++ b/lib/tty/cursor/version.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module TTY
+  module Cursor
+    VERSION = "0.7.1"
+  end # Cursor
+end # TTY
diff --git a/lib/tty/spinner.rb b/lib/tty/spinner.rb
deleted file mode 100644
index 1e419df..0000000
--- a/lib/tty/spinner.rb
+++ /dev/null
@@ -1,571 +0,0 @@
-# frozen_string_literal: true
-
-require 'monitor'
-require 'tty-cursor'
-
-require_relative 'spinner/version'
-require_relative 'spinner/formats'
-
-module TTY
-  # Used for creating terminal spinner
-  #
-  # @api public
-  class Spinner
-    include Formats
-    include MonitorMixin
-
-    # @raised when attempting to join dead thread
-    NotSpinningError = Class.new(StandardError)
-
-    ECMA_CSI = "\x1b["
-
-    MATCHER = /:spinner/
-    TICK = '✔'
-    CROSS = '✖'
-
-    CURSOR_LOCK = Monitor.new
-
-    # The object that responds to print call defaulting to stderr
-    #
-    # @api public
-    attr_reader :output
-
-    # The current format type
-    #
-    # @return [String]
-    #
-    # @api public
-    attr_reader :format
-
-    # Whether to show or hide cursor
-    #
-    # @return [Boolean]
-    #
-    # @api public
-    attr_reader :hide_cursor
-
-    # The message to print before the spinner
-    #
-    # @return [String]
-    #   the current message
-    #
-    # @api public
-    attr_reader :message
-
-    # Tokens for the message
-    #
-    # @return [Hash[Symbol, Object]]
-    #   the current tokens
-    #
-    # @api public
-    attr_reader :tokens
-
-    # The amount of time between frames in auto spinning
-    #
-    # @api public
-    attr_reader :interval
-
-    # The current row inside the multi spinner
-    #
-    # @api public
-    attr_reader :row
-
-    # Initialize a spinner
-    #
-    # @example
-    #   spinner = TTY::Spinner.new
-    #
-    # @param [String] message
-    #   the message to print in front of the spinner
-    #
-    # @param [Hash] options
-    # @option options [String] :format
-    #   the spinner format type defaulting to :spin_1
-    # @option options [Object] :output
-    #   the object that responds to print call defaulting to stderr
-    # @option options [Boolean] :hide_cursor
-    #   display or hide cursor
-    # @option options [Boolean] :clear
-    #   clear ouptut when finished
-    # @option options [Float] :interval
-    #   the interval for auto spinning
-    #
-    # @api public
-    def initialize(*args)
-      super()
-      options  = args.last.is_a?(::Hash) ? args.pop : {}
-      @message = args.empty? ? ':spinner' : args.pop
-      @tokens  = {}
-
-      @format      = options.fetch(:format) { :classic }
-      @output      = options.fetch(:output) { $stderr }
-      @hide_cursor = options.fetch(:hide_cursor) { false }
-      @frames      = options.fetch(:frames) do
-                       fetch_format(@format.to_sym, :frames)
-                     end
-      @clear       = options.fetch(:clear) { false }
-      @success_mark= options.fetch(:success_mark) { TICK }
-      @error_mark  = options.fetch(:error_mark) { CROSS }
-      @interval    = options.fetch(:interval) do
-                       fetch_format(@format.to_sym, :interval)
-                     end
-      @row         = options[:row]
-
-      @callbacks   = Hash.new { |h, k| h[k] = [] }
-      @length      = @frames.length
-      @thread      = nil
-      @job         = nil
-      @multispinner= nil
-      reset
-    end
-
-    # Reset the spinner to initial frame
-    #
-    # @api public
-    def reset
-      synchronize do
-        @current   = 0
-        @done      = false
-        @state     = :stopped
-        @succeeded = false
-        @first_run = true
-      end
-    end
-
-    # Notifies the TTY::Spinner that it is running under a multispinner
-    #
-    # @param [TTY::Spinner::Multi] the multispinner that it is running under
-    #
-    # @api private
-    def attach_to(multispinner)
-      @multispinner = multispinner
-    end
-
-    # Whether the spinner has completed spinning
-    #
-    # @return [Boolean] whether or not the spinner has finished
-    #
-    # @api public
-    def done?
-      @done
-    end
-
-    # Whether the spinner is spinning
-    #
-    # @return [Boolean] whether or not the spinner is spinning
-    #
-    # @api public
-    def spinning?
-      @state == :spinning
-    end
-
-    # Whether the spinner is in the success state.
-    # When true the spinner is marked with a success mark.
-    #
-    # @return [Boolean] whether or not the spinner succeeded
-    #
-    # @api public
-    def success?
-      @succeeded == :success
-    end
-
-    # Whether the spinner is in the error state. This is only true
-    # temporarily while it is being marked with a failure mark.
-    #
-    # @return [Boolean] whether or not the spinner is erroring
-    #
-    # @api public
-    def error?
-      @succeeded == :error
-    end
-
-    # Register callback
-    #
-    # @param [Symbol] name
-    #   the name for the event to listen for, e.i. :complete
-    #
-    # @return [self]
-    #
-    # @api public
-    def on(name, &block)
-      synchronize do
-        @callbacks[name] << block
-      end
-      self
-    end
-
-    # Start timer and unlock spinner
-    #
-    # @api public
-    def start
-      @started_at = Time.now
-      @done = false
-      reset
-    end
-
-    # Add job to this spinner
-    #
-    # @api public
-    def job(&work)
-      synchronize do
-        if block_given?
-          @job = work
-        else
-          @job
-        end
-      end
-    end
-
-    # Execute this spinner job
-    #
-    # @yield [TTY::Spinner]
-    #
-    # @api public
-    def execute_job
-      job.(self) if job?
-    end
-
-    # Check if this spinner has a scheduled job
-    #
-    # @return [Boolean]
-    #
-    # @api public
-    def job?
-      !@job.nil?
-    end
-
-    # Start automatic spinning animation
-    #
-    # @api public
-    def auto_spin
-      CURSOR_LOCK.synchronize do
-        start
-        sleep_time = 1.0 / @interval
-
-        spin
-        @thread = Thread.new do
-          sleep(sleep_time)
-          while @started_at
-            if Thread.current['pause']
-              Thread.stop
-              Thread.current['pause'] = false
-            end
-            spin
-            sleep(sleep_time)
-          end
-        end
-      end
-    ensure
-      if @hide_cursor
-        write(TTY::Cursor.show, false)
-      end
-    end
-
-    # Checked if current spinner is paused
-    #
-    # @return [Boolean]
-    #
-    # @api public
-    def paused?
-      !!(@thread && @thread['pause'])
-    end
-
-    # Pause spinner automatic animation
-    #
-    # @api public
-    def pause
-      return if paused?
-
-      synchronize do
-        @thread['pause'] = true if @thread
-      end
-    end
-
-    # Resume spinner automatic animation
-    #
-    # @api public
-    def resume
-      return unless paused?
-
-      @thread.wakeup if @thread
-    end
-
-    # Run spinner while executing job
-    #
-    # @param [String] stop_message
-    #   the message displayed when block is finished
-    #
-    # @yield automatically animate and finish spinner
-    #
-    # @example
-    #   spinner.run('Migrated DB') { ... }
-    #
-    # @api public
-    def run(stop_message = '', &block)
-      job(&block)
-      auto_spin
-
-      @work = Thread.new { execute_job }
-      @work.join
-    ensure
-      stop(stop_message)
-    end
-
-    # Duration of the spinning animation
-    #
-    # @return [Numeric]
-    #
-    # @api public
-    def duration
-      @started_at ? Time.now - @started_at : nil
-    end
-
-    # Join running spinner
-    #
-    # @param [Float] timeout
-    #   the timeout for join
-    #
-    # @api public
-    def join(timeout = nil)
-      unless @thread
-        raise(NotSpinningError, 'Cannot join spinner that is not running')
-      end
-
-      timeout ? @thread.join(timeout) : @thread.join
-    end
-
-    # Kill running spinner
-    #
-    # @api public
-    def kill
-      synchronize do
-        @thread.kill if @thread
-      end
-    end
-
-    # Perform a spin
-    #
-    # @return [String]
-    #   the printed data
-    #
-    # @api public
-    def spin
-      synchronize do
-        return if @done
-        emit(:spin)
-
-        if @hide_cursor && !spinning?
-          write(TTY::Cursor.hide)
-        end
-
-        data = message.gsub(MATCHER, @frames[@current])
-        data = replace_tokens(data)
-        write(data, true)
-        @current = (@current + 1) % @length
-        @state = :spinning
-        data
-      end
-    end
-
-    # Redraw the indent for this spinner, if it exists
-    #
-    # @api private
-    def redraw_indent
-      if @hide_cursor && !spinning?
-        write(TTY::Cursor.hide)
-      end
-
-      write("", false)
-    end
-
-    # Finish spining
-    #
-    # @param [String] stop_message
-    #   the stop message to print
-    #
-    # @api public
-    def stop(stop_message = '')
-      mon_enter
-      return if done?
-
-      clear_line
-      return if @clear
-
-      data = message.gsub(MATCHER, next_char)
-      data = replace_tokens(data)
-      if !stop_message.empty?
-        data << ' ' + stop_message
-      end
-
-      write(data, false)
-      write("\n", false) unless @clear || @multispinner
-    ensure
-      @state      = :stopped
-      @done       = true
-      @started_at = nil
-
-      if @hide_cursor
-        write(TTY::Cursor.show, false)
-      end
-
-      emit(:done)
-      kill
-      mon_exit
-    end
-
-    # Retrieve next character
-    #
-    # @return [String]
-    #
-    # @api private
-    def next_char
-      if success?
-        @success_mark
-      elsif error?
-        @error_mark
-      else
-        @frames[@current - 1]
-      end
-    end
-
-    # Finish spinning and set state to :success
-    #
-    # @api public
-    def success(stop_message = '')
-      return if done?
-
-      synchronize do
-        @succeeded = :success
-        stop(stop_message)
-        emit(:success)
-      end
-    end
-
-    # Finish spinning and set state to :error
-    #
-    # @api public
-    def error(stop_message = '')
-      return if done?
-
-      synchronize do
-        @succeeded = :error
-        stop(stop_message)
-        emit(:error)
-      end
-    end
-
-    # Clear current line
-    #
-    # @api public
-    def clear_line
-      write(ECMA_CSI + '0m' + TTY::Cursor.clear_line)
-    end
-
-    # Update string formatting tokens
-    #
-    # @param [Hash[Symbol]] tokens
-    #   the tokens used in formatting string
-    #
-    # @api public
-    def update(tokens)
-      synchronize do
-        clear_line if spinning?
-        @tokens.merge!(tokens)
-      end
-    end
-
-    private
-
-    # Execute a block on the proper terminal line if the spinner is running
-    # under a multispinner. Otherwise, execute the block on the current line.
-    #
-    # @api private
-    def execute_on_line
-      if @multispinner
-        @multispinner.synchronize do
-          if @first_run
-            @row ||= @multispinner.next_row
-            yield if block_given?
-            output.print "\n"
-            @first_run = false
-          else
-            lines_up = (@multispinner.rows + 1) - @row
-            output.print TTY::Cursor.save
-            output.print TTY::Cursor.up(lines_up)
-            yield if block_given?
-            output.print TTY::Cursor.restore
-          end
-        end
-      else
-        yield if block_given?
-      end
-    end
-
-    # Write data out to output
-    #
-    # @return [nil]
-    #
-    # @api private
-    def write(data, clear_first = false)
-      return unless tty? # write only to terminal
-
-      execute_on_line do
-        output.print(TTY::Cursor.column(1)) if clear_first
-        # If there's a top level spinner, print with inset
-        characters_in = @multispinner.line_inset(@row) if @multispinner
-        output.print("#{characters_in}#{data}")
-        output.flush
-      end
-    end
-
-    # Check if IO is attached to a terminal
-    #
-    # return [Boolean]
-    #
-    # @api public
-    def tty?
-      output.respond_to?(:tty?) && output.tty?
-    end
-
-    # Emit callback
-    #
-    # @api private
-    def emit(name, *args)
-      @callbacks[name].each do |callback|
-        callback.call(*args)
-      end
-    end
-
-    # Find frames by token name
-    #
-    # @param [Symbol] token
-    #   the name for the frames
-    #
-    # @return [Array, String]
-    #
-    # @api private
-    def fetch_format(token, property)
-      if FORMATS.key?(token)
-        FORMATS[token][property]
-      else
-        raise ArgumentError, "Unknown format token `:#{token}`"
-      end
-    end
-
-    # Replace any token inside string
-    #
-    # @param [String] string
-    #   the string containing tokens
-    #
-    # @return [String]
-    #
-    # @api private
-    def replace_tokens(string)
-      data = string.dup
-      @tokens.each do |name, val|
-        data.gsub!(/\:#{name}/, val.to_s)
-      end
-      data
-    end
-  end # Spinner
-end # TTY
diff --git a/lib/tty/spinner/formats.rb b/lib/tty/spinner/formats.rb
deleted file mode 100644
index 70bb678..0000000
--- a/lib/tty/spinner/formats.rb
+++ /dev/null
@@ -1,245 +0,0 @@
-# frozen_string_literal: true
-
-module TTY
-  module Formats
-    FORMATS = {
-      classic: {
-        interval: 10,
-        frames: %w{| / - \\}
-      },
-      spin: {
-        interval: 10,
-        frames: %w{◴ ◷ ◶ ◵ }
-      },
-      spin_2: {
-        interval: 10,
-        frames: %w{◐ ◓ ◑ ◒ }
-      },
-      spin_3: {
-        interval: 10,
-        frames: %w{◰ ◳ ◲ ◱}
-      },
-      spin_4: {
-        interval: 10,
-        frames: %w{╫ ╪}
-      },
-      pulse: {
-        interval: 10,
-        frames: %w{⎺ ⎻ ⎼ ⎽ ⎼ ⎻}
-      },
-      pulse_2: {
-        interval: 15,
-        frames: %w{▁ ▃ ▅ ▆ ▇ █ ▇ ▆ ▅ ▃ }
-      },
-      pulse_3: {
-        interval: 20,
-        frames: '▉▊▋▌▍▎▏▎▍▌▋▊▉'
-      },
-      dots: {
-        interval: 10,
-        frames: %w{⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏}
-      },
-      dots_2: {
-        interval: 10,
-        frames: %w{⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷}
-      },
-      dots_3: {
-        interval: 10,
-        frames: %w{⠋ ⠙ ⠚ ⠞ ⠖ ⠦ ⠴ ⠲ ⠳ ⠓}
-      },
-      dots_4: {
-        interval: 10,
-        frames: %w{⠄ ⠆ ⠇ ⠋ ⠙ ⠸ ⠰ ⠠ ⠰ ⠸ ⠙ ⠋ ⠇ ⠆}
-      },
-      dots_5: {
-        interval: 10,
-        frames: %w{⠋ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋}
-      },
-      dots_6: {
-        interval: 10,
-        frames: %w{⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠴ ⠲ ⠒ ⠂ ⠂ ⠒ ⠚ ⠙ ⠉ ⠁}
-      },
-      dots_7: {
-        interval: 10,
-        frames: %w{⠈ ⠉ ⠋ ⠓ ⠒ ⠐ ⠐ ⠒ ⠖ ⠦ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈}
-      },
-      dots_8: {
-        interval: 10,
-        frames: %w{⠁ ⠁ ⠉ ⠙ ⠚ ⠒ ⠂ ⠂ ⠒ ⠲ ⠴ ⠤ ⠄ ⠄ ⠤ ⠠ ⠠ ⠤ ⠦ ⠖ ⠒ ⠐ ⠐ ⠒ ⠓ ⠋ ⠉ ⠈ ⠈}
-      },
-      dots_9: {
-        interval: 10,
-        frames: %w{⢹ ⢺ ⢼ ⣸ ⣇ ⡧ ⡗ ⡏}
-      },
-      dots_10: {
-        interval: 10,
-        frames: %w{⢄ ⢂ ⢁ ⡁ ⡈ ⡐ ⡠}
-      },
-      dots_11: {
-        interval: 10,
-        frames: %w{⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈}
-      },
-      arrow: {
-        interval: 10,
-        frames: %w{← ↖ ↑ ↗ → ↘ ↓ ↙ }
-      },
-      arrow_pulse: {
-        interval: 10,
-        frames: [
-          "▹▹▹▹▹",
-          "▸▹▹▹▹",
-          "▹▸▹▹▹",
-          "▹▹▸▹▹",
-          "▹▹▹▸▹",
-          "▹▹▹▹▸"
-        ]
-      },
-      triangle: {
-        interval: 10,
-        frames: %w{◢ ◣ ◤ ◥}
-      },
-      arc: {
-        interval: 10,
-        frames: %w{ ◜ ◠ ◝ ◞ ◡ ◟ }
-      },
-      pipe: {
-        interval: 10,
-        frames: %w{ ┤ ┘ ┴ └ ├ ┌ ┬ ┐ }
-      },
-      bouncing: {
-        interval: 10,
-        frames: [
-          "[    ]",
-          "[   =]",
-          "[  ==]",
-          "[ ===]",
-          "[====]",
-          "[=== ]",
-          "[==  ]",
-          "[=   ]"
-        ]
-      },
-      bouncing_ball: {
-        interval: 10,
-        frames: [
-          "( ●    )",
-          "(  ●   )",
-          "(   ●  )",
-          "(    ● )",
-          "(     ●)",
-          "(    ● )",
-          "(   ●  )",
-          "(  ●   )",
-          "( ●    )",
-          "(●     )"
-        ]
-      },
-      bounce: {
-        interval: 10,
-        frames: %w{ ⠁ ⠂ ⠄ ⠂ }
-      },
-      box_bounce: {
-        interval: 10,
-        frames: %w{ ▌ ▀ ▐ ▄  }
-      },
-      box_bounce_2: {
-        interval: 10,
-        frames: %w{ ▖ ▘ ▝ ▗ }
-      },
-      star: {
-        interval: 10,
-        frames: %w{ ✶ ✸ ✹ ✺ ✹ ✷ }
-      },
-      toggle: {
-        interval: 10,
-        frames: %w{ ■ □ ▪ ▫ }
-      },
-      balloon: {
-        interval: 10,
-        frames: %w{ . o O @ * }
-      },
-      balloon_2: {
-        interval: 10,
-        frames: %w{. o O ° O o . }
-      },
-      flip: {
-        interval: 10,
-        frames: '-◡⊙-◠'
-      },
-      burger: {
-        interval: 6,
-        frames: %w{ ☱ ☲ ☴ }
-      },
-      dance: {
-        interval: 10,
-        frames: [">))'>", " >))'>", "  >))'>", "   >))'>", "    >))'>", "   <'((<", "  <'((<", " <'((<"]
-      },
-      shark: {
-        interval: 10,
-        frames: [
-          "▐|\\____________▌",
-          "▐_|\\___________▌",
-          "▐__|\\__________▌",
-          "▐___|\\_________▌",
-          "▐____|\\________▌",
-          "▐_____|\\_______▌",
-          "▐______|\\______▌",
-          "▐_______|\\_____▌",
-          "▐________|\\____▌",
-          "▐_________|\\___▌",
-          "▐__________|\\__▌",
-          "▐___________|\\_▌",
-          "▐____________|\\▌",
-          "▐____________/|▌",
-          "▐___________/|_▌",
-          "▐__________/|__▌",
-          "▐_________/|___▌",
-          "▐________/|____▌",
-          "▐_______/|_____▌",
-          "▐______/|______▌",
-          "▐_____/|_______▌",
-          "▐____/|________▌",
-          "▐___/|_________▌",
-          "▐__/|__________▌",
-          "▐_/|___________▌",
-          "▐/|____________▌"
-        ]
-      },
-      pong: {
-        interval: 10,
-        frames: [
-          "▐⠂       ▌",
-          "▐⠈       ▌",
-          "▐ ⠂      ▌",
-          "▐ ⠠      ▌",
-          "▐  ⡀     ▌",
-          "▐  ⠠     ▌",
-          "▐   ⠂    ▌",
-          "▐   ⠈    ▌",
-          "▐    ⠂   ▌",
-          "▐    ⠠   ▌",
-          "▐     ⡀  ▌",
-          "▐     ⠠  ▌",
-          "▐      ⠂ ▌",
-          "▐      ⠈ ▌",
-          "▐       ⠂▌",
-          "▐       ⠠▌",
-          "▐       ⡀▌",
-          "▐      ⠠ ▌",
-          "▐      ⠂ ▌",
-          "▐     ⠈  ▌",
-          "▐     ⠂  ▌",
-          "▐    ⠠   ▌",
-          "▐    ⡀   ▌",
-          "▐   ⠠    ▌",
-          "▐   ⠂    ▌",
-          "▐  ⠈     ▌",
-          "▐  ⠂     ▌",
-          "▐ ⠠      ▌",
-          "▐ ⡀      ▌",
-          "▐⠠       ▌"
-        ]
-      }
-    }
-  end # Formats
-end # TTY
diff --git a/lib/tty/spinner/multi.rb b/lib/tty/spinner/multi.rb
deleted file mode 100644
index c1cf2db..0000000
--- a/lib/tty/spinner/multi.rb
+++ /dev/null
@@ -1,353 +0,0 @@
-# frozen_string_literal: true
-
-require 'monitor'
-require 'forwardable'
-
-require_relative '../spinner'
-
-module TTY
-  class Spinner
-    # Used for managing multiple terminal spinners
-    #
-    # @api public
-    class Multi
-      include Enumerable
-      include MonitorMixin
-
-      extend Forwardable
-
-      def_delegators :@spinners, :each, :empty?, :length
-
-      DEFAULT_INSET = {
-        top:    Gem.win_platform? ? '+ '   : "\u250c ",
-        middle: Gem.win_platform? ? '|-- ' : "\u251c\u2500\u2500 ",
-        bottom: Gem.win_platform? ? '|__ ' : "\u2514\u2500\u2500 "
-      }
-
-      # The current count of all rendered rows
-      #
-      # @api public
-      attr_reader :rows
-
-      # Initialize a multispinner
-      #
-      # @example
-      #   spinner = TTY::Spinner::Multi.new
-      #
-      # @param [String] message
-      #   the optional message to print in front of the top level spinner
-      #
-      # @param [Hash] options
-      # @option options [Hash] :style
-      #   keys :top :middle and :bottom can contain Strings that are used to
-      #   indent the spinners. Ignored if message is blank
-      # @option options [Object] :output
-      #   the object that responds to print call defaulting to stderr
-      # @option options [Boolean] :hide_cursor
-      #   display or hide cursor
-      # @option options [Boolean] :clear
-      #   clear ouptut when finished
-      # @option options [Float] :interval
-      #   the interval for auto spinning
-      #
-      # @api public
-      def initialize(*args)
-        super()
-        @options = args.last.is_a?(::Hash) ? args.pop : {}
-        message = args.empty? ? nil : args.pop
-        @inset_opts  = @options.delete(:style) { DEFAULT_INSET }
-        @rows        = 0
-        @spinners    = []
-        @spinners_count = 0
-        @top_spinner = nil
-        @last_spin_at = nil
-        unless message.nil?
-          @top_spinner = register(message, observable: false, row: next_row)
-        end
-
-        @callbacks = {
-          success: [],
-          error:   [],
-          done:    [],
-          spin:    []
-        }
-      end
-
-      # Register a new spinner
-      #
-      # @param [String, TTY::Spinner] pattern_or_spinner
-      #   the pattern used for creating spinner, or a spinner instance
-      #
-      # @api public
-      def register(pattern_or_spinner, **options, &job)
-        observable = options.delete(:observable) { true }
-        spinner = nil
-
-        synchronize do
-          spinner = create_spinner(pattern_or_spinner, options)
-          spinner.attach_to(self)
-          spinner.job(&job) if block_given?
-          observe(spinner) if observable
-          @spinners << spinner
-          @spinners_count += 1
-          if @top_spinner
-            @spinners.each { |sp| sp.redraw_indent if sp.spinning? || sp.done? }
-          end
-        end
-
-        spinner
-      end
-
-      # Create a spinner instance
-      #
-      # @api private
-      def create_spinner(pattern_or_spinner, options)
-        case pattern_or_spinner
-        when ::String
-          TTY::Spinner.new(
-            pattern_or_spinner,
-            @options.merge(options)
-          )
-        when ::TTY::Spinner
-          pattern_or_spinner
-        else
-          raise ArgumentError, "Expected a pattern or spinner, " \
-            "got: #{pattern_or_spinner.class}"
-        end
-      end
-
-      # Increase a row count
-      #
-      # @api public
-      def next_row
-        synchronize do
-          @rows += 1
-        end
-      end
-
-      # Get the top level spinner if it exists
-      #
-      # @return [TTY::Spinner] the top level spinner
-      #
-      # @api public
-      def top_spinner
-        raise "No top level spinner" if @top_spinner.nil?
-
-        @top_spinner
-      end
-
-      # Auto spin the top level spinner & all child spinners
-      # that have scheduled jobs
-      #
-      # @api public
-      def auto_spin
-        raise "No top level spinner" if @top_spinner.nil?
-
-        jobs = []
-        @spinners.each do |spinner|
-          if spinner.job?
-            spinner.auto_spin
-            jobs << Thread.new { spinner.execute_job }
-          end
-        end
-        jobs.each(&:join)
-      end
-
-      # Perform a single spin animation
-      #
-      # @api public
-      def spin
-        raise "No top level spinner" if @top_spinner.nil?
-
-        synchronize do
-          throttle { @top_spinner.spin }
-        end
-      end
-
-      # Pause all spinners
-      #
-      # @api public
-      def pause
-        @spinners.dup.each(&:pause)
-      end
-
-      # Resume all spinners
-      #
-      # @api public
-      def resume
-        @spinners.dup.each(&:resume)
-      end
-
-      # Find the number of characters to move into the line
-      # before printing the spinner
-      #
-      # @param [Integer] line_no
-      #   the current spinner line number for which line inset is calculated
-      #
-      # @return [String]
-      #   the inset
-      #
-      # @api public
-      def line_inset(line_no)
-        return '' if @top_spinner.nil?
-
-        if line_no == 1
-          @inset_opts[:top]
-        elsif line_no == @spinners_count
-          @inset_opts[:bottom]
-        else
-          @inset_opts[:middle]
-        end
-      end
-
-      # Check if all spinners are done
-      #
-      # @return [Boolean]
-      #
-      # @api public
-      def done?
-        synchronize do
-          (@spinners - [@top_spinner]).all?(&:done?)
-        end
-      end
-
-      # Check if all spinners succeeded
-      #
-      # @return [Boolean]
-      #
-      # @api public
-      def success?
-        synchronize do
-          (@spinners - [@top_spinner]).all?(&:success?)
-        end
-      end
-
-      # Check if any spinner errored
-      #
-      # @return [Boolean]
-      #
-      # @api public
-      def error?
-        synchronize do
-          (@spinners - [@top_spinner]).any?(&:error?)
-        end
-      end
-
-      # Stop all spinners
-      #
-      # @api public
-      def stop
-        @spinners.dup.each(&:stop)
-      end
-
-      # Stop all spinners with success status
-      #
-      # @api public
-      def success
-        @spinners.dup.each(&:success)
-      end
-
-      # Stop all spinners with error status
-      #
-      # @api public
-      def error
-        @spinners.dup.each(&:error)
-      end
-
-      # Listen on event
-      #
-      # @api public
-      def on(key, &callback)
-        unless @callbacks.key?(key)
-          raise ArgumentError, "The event #{key} does not exist. "\
-                               ' Use :spin, :success, :error, or :done instead'
-        end
-        @callbacks[key] << callback
-        self
-      end
-
-      private
-
-      # Check if this spinner should revolve to keep constant speed
-      # matching top spinner interval
-      #
-      # @api private
-      def throttle
-        sleep_time = 1.0 / @top_spinner.interval
-        if @last_spin_at && Time.now - @last_spin_at < sleep_time
-          return
-        end
-        yield if block_given?
-        @last_spin_at = Time.now
-      end
-
-      # Fire an event
-      #
-      # @api private
-      def emit(key, *args)
-        @callbacks[key].each do |block|
-          block.call(*args)
-        end
-      end
-
-      # Observe spinner for events to notify top spinner of current state
-      #
-      # @param [TTY::Spinner] spinner
-      #   the spinner to listen to for events
-      #
-      # @api private
-      def observe(spinner)
-        spinner.on(:spin, &spin_handler)
-               .on(:success, &success_handler)
-               .on(:error, &error_handler)
-               .on(:done, &done_handler)
-      end
-
-      # Handle spin event
-      #
-      # @api private
-      def spin_handler
-        proc do
-          spin if @top_spinner
-          emit(:spin)
-        end
-      end
-
-      # Handle the success state
-      #
-      # @api private
-      def success_handler
-        proc do
-          if success?
-            @top_spinner.success if @top_spinner
-            emit(:success)
-          end
-        end
-      end
-
-      # Handle the error state
-      #
-      # @api private
-      def error_handler
-        proc do
-          if error?
-            @top_spinner.error if @top_spinner
-            @fired ||= emit(:error) # fire once
-          end
-        end
-      end
-
-      # Handle the done state
-      #
-      # @api private
-      def done_handler
-        proc do
-          if done?
-            @top_spinner.stop if @top_spinner && !error? && !success?
-            emit(:done)
-          end
-        end
-      end
-    end # MultiSpinner
-  end # Spinner
-end # TTY
diff --git a/lib/tty/spinner/version.rb b/lib/tty/spinner/version.rb
deleted file mode 100644
index e709c97..0000000
--- a/lib/tty/spinner/version.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module TTY
-  class Spinner
-    VERSION = "0.9.3"
-  end # Spinner
-end # TTY
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..ffbb08c
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+if ENV["COVERAGE"] == "true"
+  require "simplecov"
+  require "coveralls"
+
+  SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
+    SimpleCov::Formatter::HTMLFormatter,
+    Coveralls::SimpleCov::Formatter
+  ])
+
+  SimpleCov.start do
+    command_name "spec"
+    add_filter "spec"
+  end
+end
+
+require "tty-cursor"
+
+RSpec.configure do |config|
+  config.expect_with :rspec do |expectations|
+    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+  end
+
+  config.mock_with :rspec do |mocks|
+    mocks.verify_partial_doubles = true
+  end
+
+  # Limits the available syntax to the non-monkey patched syntax that is recommended.
+  config.disable_monkey_patching!
+
+  # This setting enables warnings. It's recommended, but in some cases may
+  # be too noisy due to issues in dependencies.
+  config.warnings = true
+
+  if config.files_to_run.one?
+    config.default_formatter = "doc"
+  end
+
+  config.profile_examples = 2
+
+  config.order = :random
+
+  Kernel.srand config.seed
+end
diff --git a/spec/unit/clear_lines_spec.rb b/spec/unit/clear_lines_spec.rb
new file mode 100644
index 0000000..7f1006b
--- /dev/null
+++ b/spec/unit/clear_lines_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+RSpec.describe TTY::Cursor, "#clear_lines" do
+  subject(:cursor) { described_class }
+
+  it "clears character" do
+    expect(cursor.clear_char).to eq("\e[X")
+  end
+
+  it "clears few characters" do
+    expect(cursor.clear_char(5)).to eq("\e[5X")
+  end
+
+  it "clears line" do
+    expect(cursor.clear_line).to eq("\e[2K\e[1G")
+  end
+
+  it "clears the line before the cursor" do
+    expect(cursor.clear_line_before).to eq("\e[1K")
+  end
+
+  it "clears the line after the cursor" do
+    expect(cursor.clear_line_after).to eq("\e[0K")
+  end
+
+  it "clears 5 lines up" do
+    expect(cursor.clear_lines(5)).to eq([
+      "\e[2K\e[1G\e[1A",
+      "\e[2K\e[1G\e[1A",
+      "\e[2K\e[1G\e[1A",
+      "\e[2K\e[1G\e[1A",
+      "\e[2K\e[1G"
+    ].join)
+  end
+
+  it "clears 5 lines down" do
+    expect(cursor.clear_lines(5, :down)).to eq([
+      "\e[2K\e[1G\e[1B",
+      "\e[2K\e[1G\e[1B",
+      "\e[2K\e[1G\e[1B",
+      "\e[2K\e[1G\e[1B",
+      "\e[2K\e[1G"
+    ].join)
+  end
+
+  it "clears screen down" do
+    expect(cursor.clear_screen_down).to eq("\e[J")
+  end
+
+  it "clears screen up" do
+    expect(cursor.clear_screen_up).to eq("\e[1J")
+  end
+
+  it "clears entire screen" do
+    expect(cursor.clear_screen).to eq("\e[2J")
+  end
+end
diff --git a/spec/unit/cursor_spec.rb b/spec/unit/cursor_spec.rb
new file mode 100644
index 0000000..2505718
--- /dev/null
+++ b/spec/unit/cursor_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+RSpec.describe TTY::Cursor do
+  subject(:cursor) { described_class }
+
+  it "shows cursor" do
+    expect(cursor.show).to eq("\e[?25h")
+  end
+
+  it "hides cursor" do
+    expect(cursor.hide).to eq("\e[?25l")
+  end
+
+  it "saves cursor position" do
+    allow(Gem).to receive(:win_platform?).and_return(false)
+
+    expect(cursor.save).to eq("\e7")
+  end
+
+  it "saves cursor position on Windows" do
+    allow(Gem).to receive(:win_platform?).and_return(true)
+
+    expect(cursor.save).to eq("\e[s")
+  end
+
+  it "restores cursor position" do
+    allow(Gem).to receive(:win_platform?).and_return(false)
+
+    expect(cursor.restore).to eq("\e8")
+  end
+
+  it "restores cursor position on Windows" do
+    allow(Gem).to receive(:win_platform?).and_return(true)
+
+    expect(cursor.restore).to eq("\e[u")
+  end
+
+  it "gets current cursor position" do
+    expect(cursor.current).to eq("\e[6n")
+  end
+
+  it "moves cursor up default by 1 line" do
+    expect(cursor.up).to eq("\e[1A")
+  end
+
+  it "moves cursor up by 5 lines" do
+    expect(cursor.up(5)).to eq("\e[5A")
+  end
+
+  it "moves cursor down default by 1 line" do
+    expect(cursor.down).to eq("\e[1B")
+  end
+
+  it "moves cursor down by 5 lines" do
+    expect(cursor.down(5)).to eq("\e[5B")
+  end
+
+  it "moves cursorleft by 1 line default" do
+    expect(cursor.backward).to eq("\e[1D")
+  end
+
+  it "moves cursor left by 5" do
+    expect(cursor.backward(5)).to eq("\e[5D")
+  end
+
+  it "moves cursor right by 1 line default" do
+    expect(cursor.forward).to eq("\e[1C")
+  end
+
+  it "moves cursor right by 5 lines" do
+    expect(cursor.forward(5)).to eq("\e[5C")
+  end
+
+  it "moves cursor horizontal to start" do
+    expect(cursor.column).to eq("\e[1G")
+  end
+
+  it "moves cursor horizontally to 66th position" do
+    expect(cursor.column(66)).to eq("\e[66G")
+  end
+
+  it "moves cursor vertically to start" do
+    expect(cursor.row).to eq("\e[1d")
+  end
+
+  it "moves cursor vertically to 50th row" do
+    expect(cursor.row(50)).to eq("\e[50d")
+  end
+
+  it "moves cursor to next line" do
+    expect(cursor.next_line).to eq("\e[E\e[1G")
+  end
+
+  it "moves cursor to previous line" do
+    expect(cursor.prev_line).to eq("\e[A\e[1G")
+  end
+
+  it "hides cursor for the duration of block call" do
+    stream = StringIO.new
+    expect { |block|
+      cursor.invisible(stream, &block)
+    }.to yield_with_no_args
+    expect(stream.string).to eq("\e[?25l\e[?25h")
+  end
+
+  it "shows hidden cursor on failure inside a block" do
+    stream = StringIO.new
+    expect {
+      cursor.invisible(stream) { raise "boom" }
+    }.to raise_error("boom")
+    expect(stream.string).to eq("\e[?25l\e[?25h")
+  end
+end
diff --git a/spec/unit/move_spec.rb b/spec/unit/move_spec.rb
new file mode 100644
index 0000000..f5ffdea
--- /dev/null
+++ b/spec/unit/move_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+RSpec.describe TTY::Cursor, "#move" do
+  subject(:cursor) { described_class }
+
+  it "doesn't move for point (0, 0)" do
+    expect(cursor.move(0, 0)).to eq("")
+  end
+
+  it "moves only to the right" do
+    expect(cursor.move(2, 0)).to eq("\e[2C")
+  end
+
+  it "moves right and up" do
+    expect(cursor.move(2, 3)).to eq("\e[2C\e[3A")
+  end
+
+  it "moves right and down" do
+    expect(cursor.move(2, -3)).to eq("\e[2C\e[3B")
+  end
+
+  it "moves left and up" do
+    expect(cursor.move(-2, 3)).to eq("\e[2D\e[3A")
+  end
+
+  it "moves left and down" do
+    expect(cursor.move(-2, -3)).to eq("\e[2D\e[3B")
+  end
+end
diff --git a/spec/unit/move_to_spec.rb b/spec/unit/move_to_spec.rb
new file mode 100644
index 0000000..c5fe789
--- /dev/null
+++ b/spec/unit/move_to_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.describe TTY::Cursor, "#move_to" do
+  subject(:cursor) { described_class }
+
+  it "moves to home" do
+    expect(cursor.move_to).to eq("\e[H")
+  end
+
+  it "moves to row and column" do
+    expect(cursor.move_to(2, 3)).to eq("\e[4;3H")
+  end
+end
diff --git a/spec/unit/scroll_spec.rb b/spec/unit/scroll_spec.rb
new file mode 100644
index 0000000..a49ea6a
--- /dev/null
+++ b/spec/unit/scroll_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.describe TTY::Cursor do
+  subject(:cursor) { described_class }
+
+  it "scrolls down by one line" do
+    expect(cursor.scroll_down).to eq("\eD")
+  end
+
+  it "scrolls up by one line" do
+    expect(cursor.scroll_up).to eq("\eM")
+  end
+end
diff --git a/tasks/console.rake b/tasks/console.rake
new file mode 100644
index 0000000..f4466b1
--- /dev/null
+++ b/tasks/console.rake
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+desc "Load gem inside irb console"
+task :console do
+  require 'irb'
+  require 'irb/completion'
+  require File.join(__FILE__, '../../lib/tty-cursor')
+  ARGV.clear
+  IRB.start
+end
+task c: %w[ console ]
diff --git a/tasks/coverage.rake b/tasks/coverage.rake
new file mode 100644
index 0000000..e7c9050
--- /dev/null
+++ b/tasks/coverage.rake
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+desc 'Measure code coverage'
+task :coverage do
+  begin
+    original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true'
+    Rake::Task['spec'].invoke
+  ensure
+    ENV['COVERAGE'] = original
+  end
+end
diff --git a/tasks/spec.rake b/tasks/spec.rake
new file mode 100644
index 0000000..c4373c8
--- /dev/null
+++ b/tasks/spec.rake
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+begin
+  require 'rspec/core/rake_task'
+
+  desc 'Run all specs'
+  RSpec::Core::RakeTask.new(:spec) do |task|
+    task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb'
+  end
+
+  namespace :spec do
+    desc 'Run unit specs'
+    RSpec::Core::RakeTask.new(:unit) do |task|
+      task.pattern = 'spec/unit{,/*/**}/*_spec.rb'
+    end
+
+    desc 'Run integration specs'
+    RSpec::Core::RakeTask.new(:integration) do |task|
+      task.pattern = 'spec/integration{,/*/**}/*_spec.rb'
+    end
+  end
+
+rescue LoadError
+  %w[spec spec:unit spec:integration].each do |name|
+    task name do
+      $stderr.puts "In order to run #{name}, do `gem install rspec`"
+    end
+  end
+end
diff --git a/tty-cursor.gemspec b/tty-cursor.gemspec
new file mode 100644
index 0000000..71a180b
--- /dev/null
+++ b/tty-cursor.gemspec
@@ -0,0 +1,31 @@
+require_relative "lib/tty/cursor/version"
+
+Gem::Specification.new do |spec|
+  spec.name          = "tty-cursor"
+  spec.version       = TTY::Cursor::VERSION
+  spec.authors       = ["Piotr Murach"]
+  spec.email         = ["piotr@piotrmurach.com"]
+  spec.summary       = %q{Terminal cursor positioning, visibility and text manipulation.}
+  spec.description   = %q{The purpose of this library is to help move the terminal cursor around and manipulate text by using intuitive method calls.}
+  spec.homepage       = "https://ttytoolkit.org"
+  spec.license       = "MIT"
+  if spec.respond_to?(:metadata=)
+    spec.metadata = {
+      "allowed_push_host" => "https://rubygems.org",
+      "bug_tracker_uri"   => "https://github.com/piotrmurach/tty-cursor/issues",
+      "changelog_uri"     => "https://github.com/piotrmurach/tty-cursor/blob/master/CHANGELOG.md",
+      "documentation_uri" => "https://www.rubydoc.info/gems/tty-cursor",
+      "homepage_uri"      => spec.homepage,
+      "source_code_uri"   => "https://github.com/piotrmurach/tty-cursor"
+    }
+  end
+  spec.files         = Dir["lib/**/*", "README.md", "CHANGELOG.md", "LICENSE.txt"]
+  spec.extra_rdoc_files = ["README.md", "CHANGELOG.md"]
+  spec.bindir        = "exe"
+  spec.require_paths = ["lib"]
+  spec.required_ruby_version = ">= 2.0.0"
+
+  spec.add_development_dependency "bundler", ">= 1.6"
+  spec.add_development_dependency "rspec", "~> 3.1"
+  spec.add_development_dependency "rake"
+end
diff --git a/tty-spinner.gemspec b/tty-spinner.gemspec
deleted file mode 100644
index 08b1d4d..0000000
--- a/tty-spinner.gemspec
+++ /dev/null
@@ -1,38 +0,0 @@
-#########################################################
-# This file has been automatically generated by gem2tgz #
-#########################################################
-# -*- encoding: utf-8 -*-
-# stub: tty-spinner 0.9.3 ruby lib
-
-Gem::Specification.new do |s|
-  s.name = "tty-spinner".freeze
-  s.version = "0.9.3"
-
-  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
-  s.metadata = { "allowed_push_host" => "https://rubygems.org", "bug_tracker_uri" => "https://github.com/piotrmurach/tty-spinner/issues", "changelog_uri" => "https://github.com/piotrmurach/tty-spinner/blob/master/CHANGELOG.md", "documentation_uri" => "https://www.rubydoc.info/gems/tty-spinner", "homepage_uri" => "https://ttytoolkit.org", "source_code_uri" => "https://github.com/piotrmurach/tty-spinner" } if s.respond_to? :metadata=
-  s.require_paths = ["lib".freeze]
-  s.authors = ["Piotr Murach".freeze]
-  s.bindir = "exe".freeze
-  s.date = "2020-01-28"
-  s.description = "A terminal spinner for tasks that have non-deterministic time frame.".freeze
-  s.email = ["piotr@piotrmurach.com".freeze]
-  s.extra_rdoc_files = ["CHANGELOG.md".freeze, "README.md".freeze]
-  s.files = ["CHANGELOG.md".freeze, "LICENSE.txt".freeze, "README.md".freeze, "lib/tty-spinner.rb".freeze, "lib/tty/spinner.rb".freeze, "lib/tty/spinner/formats.rb".freeze, "lib/tty/spinner/multi.rb".freeze, "lib/tty/spinner/version.rb".freeze]
-  s.homepage = "https://ttytoolkit.org".freeze
-  s.licenses = ["MIT".freeze]
-  s.required_ruby_version = Gem::Requirement.new(">= 2.0.0".freeze)
-  s.rubygems_version = "2.7.6.2".freeze
-  s.summary = "A terminal spinner for tasks that have non-deterministic time frame.".freeze
-
-  if s.respond_to? :specification_version then
-    s.specification_version = 4
-
-    if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
-      s.add_runtime_dependency(%q<tty-cursor>.freeze, ["~> 0.7"])
-    else
-      s.add_dependency(%q<tty-cursor>.freeze, ["~> 0.7"])
-    end
-  else
-    s.add_dependency(%q<tty-cursor>.freeze, ["~> 0.7"])
-  end
-end

Debdiff

[The following lists of changes regard files as different if they have different names, permissions or owners.]

Files in second set of .debs but not in first

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/tty-cursor-0.7.1/lib/tty-cursor.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/tty-cursor-0.7.1/lib/tty/cursor.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/tty-cursor-0.7.1/lib/tty/cursor/version.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/tty-cursor-0.7.1.gemspec

Files in first set of .debs but not in second

-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/tty-spinner-0.9.3/lib/tty-spinner.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/tty-spinner-0.9.3/lib/tty/spinner.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/tty-spinner-0.9.3/lib/tty/spinner/formats.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/tty-spinner-0.9.3/lib/tty/spinner/multi.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/gems/tty-spinner-0.9.3/lib/tty/spinner/version.rb
-rw-r--r--  root/root   /usr/share/rubygems-integration/all/specifications/tty-spinner-0.9.3.gemspec

Control files: lines which differ (wdiff format)

  • Depends: ruby-tty-cursor (>= 0.7)

More details

Full run details