starlined 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +48 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/Rakefile +31 -0
- data/examples/animated_commands.rb +37 -0
- data/examples/basic_messages.rb +36 -0
- data/examples/custom_animation.rb +61 -0
- data/examples/parallel_execution.rb +63 -0
- data/lib/starlined/animation.rb +158 -0
- data/lib/starlined/configuration.rb +17 -0
- data/lib/starlined/messages.rb +54 -0
- data/lib/starlined/runner.rb +110 -0
- data/lib/starlined/version.rb +5 -0
- data/lib/starlined.rb +28 -0
- data/starlined.gemspec +36 -0
- metadata +141 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0a1a5ed5721f3b9e5dec80d748357e822aa1b4076873e163d66df7978aa65ea5
|
|
4
|
+
data.tar.gz: a524ffdf7cc20b93e3a512dc8cb1d297087bedce1da6e6e1b9da807e6c5c2e1e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: cc56e5e9985998254ebc191c12c2d7c736fad957c02c7a77beef1511bac03204598e5f3a5dc1127d00ca4cb2bcd3c6d378cd97ce4d1af4ce854f82ba9999eee8
|
|
7
|
+
data.tar.gz: 25f53f0ea6f5342dd5a45810229c0c24e5adbb0bf96f34885da488285932bb7aaca792dafa4a1b93b7e75bb85356bca3e6c68c126eebaa7e0a3de697d2d610aa
|
data/.gitignore
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/spec/examples.txt
|
|
9
|
+
/test/tmp/
|
|
10
|
+
/test/version_tmp/
|
|
11
|
+
/tmp/
|
|
12
|
+
|
|
13
|
+
# Used by dotenv library to load environment variables
|
|
14
|
+
# .env
|
|
15
|
+
|
|
16
|
+
# Ignore Byebug command history file
|
|
17
|
+
.byebug_history
|
|
18
|
+
|
|
19
|
+
## Specific to RubyMotion:
|
|
20
|
+
.dat*
|
|
21
|
+
.repl_history
|
|
22
|
+
build/
|
|
23
|
+
*.bridgesupport
|
|
24
|
+
build-iPhoneOS/
|
|
25
|
+
build-iPhoneSimulator/
|
|
26
|
+
|
|
27
|
+
## Documentation cache and generated files:
|
|
28
|
+
/.yardoc/
|
|
29
|
+
/_yardoc/
|
|
30
|
+
/doc/
|
|
31
|
+
/rdoc/
|
|
32
|
+
|
|
33
|
+
## Environment normalization:
|
|
34
|
+
/.bundle/
|
|
35
|
+
/vendor/bundle
|
|
36
|
+
/lib/bundler/man/
|
|
37
|
+
|
|
38
|
+
# for a library or gem, you might want to ignore these files since the code is
|
|
39
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
40
|
+
Gemfile.lock
|
|
41
|
+
.ruby-version
|
|
42
|
+
.ruby-gemset
|
|
43
|
+
|
|
44
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
45
|
+
.rvmrc
|
|
46
|
+
|
|
47
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive
|
|
48
|
+
# .rubocop-https?--*
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Starlined
|
|
2
|
+
|
|
3
|
+
A Ruby gem that provides beautiful terminal output utilities with animated star progress indicators, colored messages, and command execution with visual feedback.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🌟 **Animated Progress Indicators** - Beautiful star animations that move while commands execute
|
|
8
|
+
- 🎨 **Colored Messages** - Error, info, warning, and success messages with appropriate colors
|
|
9
|
+
- 🔄 **Thread-Safe Operations** - Safe to use with parallel command execution
|
|
10
|
+
- ⚡ **Command Runner** - Execute shell commands with automatic progress animation
|
|
11
|
+
- 🔧 **Highly Configurable** - Customize animation speed, message formatting, and more
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add this line to your application's Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem 'starlined'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
And then execute:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install it yourself as:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gem install starlined
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Basic Message Output
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require 'starlined'
|
|
39
|
+
|
|
40
|
+
# include the messages module for easy access
|
|
41
|
+
include Starlined::Messages
|
|
42
|
+
|
|
43
|
+
# display various message types
|
|
44
|
+
info("Starting application...")
|
|
45
|
+
warn("Configuration file not found, using defaults")
|
|
46
|
+
error("Connection failed", "Database timeout")
|
|
47
|
+
success("Build completed", 3.5) # with elapsed time
|
|
48
|
+
verbose("Debug information") # only shown if verbose mode is enabled
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Animated Command Execution
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
runner = Starlined::Runner.new
|
|
55
|
+
|
|
56
|
+
# start an animation with a message
|
|
57
|
+
runner.start("Installing dependencies", steps: 3)
|
|
58
|
+
|
|
59
|
+
# run commands with animation
|
|
60
|
+
runner.run("bundle install", aka: "bundler")
|
|
61
|
+
runner.run("npm install", aka: "npm")
|
|
62
|
+
runner.run("rake db:migrate", aka: "migrations")
|
|
63
|
+
|
|
64
|
+
# stop animation and show success
|
|
65
|
+
runner.stop
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Interactive Input
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
include Starlined::Messages
|
|
72
|
+
|
|
73
|
+
response = ask("What's your project name?")
|
|
74
|
+
puts "Creating project: #{response}"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Configuration
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
Starlined.configure do |config|
|
|
81
|
+
config.verbose = true # enable verbose output
|
|
82
|
+
config.sleep_time = 0.5 # animation speed (seconds)
|
|
83
|
+
config.msg_ljust = 40 # message padding
|
|
84
|
+
config.stars_range = (0..7) # animation width
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Advanced Usage with Parallel Execution
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
runner = Starlined::Runner.new
|
|
92
|
+
runner.start("Running tests", steps: 3)
|
|
93
|
+
|
|
94
|
+
threads = []
|
|
95
|
+
threads << Thread.new { runner.run("rspec spec/models", aka: "models") }
|
|
96
|
+
threads << Thread.new { runner.run("rspec spec/controllers", aka: "controllers") }
|
|
97
|
+
threads << Thread.new { runner.run("rspec spec/helpers", aka: "helpers") }
|
|
98
|
+
|
|
99
|
+
threads.each(&:join)
|
|
100
|
+
runner.stop
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Using Animation Directly
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
animation = Starlined::Animation.new("Processing files", steps: 100)
|
|
107
|
+
animation.start
|
|
108
|
+
|
|
109
|
+
100.times do |i|
|
|
110
|
+
# do some work
|
|
111
|
+
sleep(0.1)
|
|
112
|
+
animation.increment_step
|
|
113
|
+
animation.add_alias("file_#{i}.txt")
|
|
114
|
+
# more work
|
|
115
|
+
animation.remove_alias("file_#{i}.txt")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
animation.stop
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Configuration Options
|
|
122
|
+
|
|
123
|
+
| Option | Default | Description |
|
|
124
|
+
|--------|---------|-------------|
|
|
125
|
+
| `sleep_time` | `0.75` | Time between animation frames (seconds) |
|
|
126
|
+
| `msg_ljust` | `30` | Left padding for messages |
|
|
127
|
+
| `extra_rjust` | `8` | Right padding for extra info |
|
|
128
|
+
| `stars_range` | `(0..5)` | Range for star animation positions |
|
|
129
|
+
| `verbose` | `false` | Enable verbose output |
|
|
130
|
+
| `clear_line_string` | `"\33[2K"` | ANSI code to clear terminal line |
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
|
|
135
|
+
|
|
136
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
137
|
+
|
|
138
|
+
## Contributing
|
|
139
|
+
|
|
140
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/starlined.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
145
|
+
|
|
146
|
+
## Author
|
|
147
|
+
|
|
148
|
+
Created by Mier - Extracted from the Okticket builder script utilities.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
task default: :spec
|
|
9
|
+
|
|
10
|
+
desc "Run all examples"
|
|
11
|
+
task :examples do
|
|
12
|
+
Dir["examples/*.rb"].each do |example|
|
|
13
|
+
puts "\n📝 Running: #{example}"
|
|
14
|
+
puts "=" * 50
|
|
15
|
+
system("ruby #{example}")
|
|
16
|
+
puts "=" * 50
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "Open an interactive console with the gem loaded"
|
|
21
|
+
task :console do
|
|
22
|
+
require "pry"
|
|
23
|
+
require_relative "lib/starlined"
|
|
24
|
+
include Starlined::Messages
|
|
25
|
+
runner = Starlined::Runner.new
|
|
26
|
+
puts "Starlined loaded! Available:"
|
|
27
|
+
puts " - Messages module included (info, warn, error, success, etc.)"
|
|
28
|
+
puts " - runner = Starlined::Runner.new"
|
|
29
|
+
puts " - Starlined::Animation"
|
|
30
|
+
Pry.start
|
|
31
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/starlined"
|
|
5
|
+
|
|
6
|
+
runner = Starlined::Runner.new
|
|
7
|
+
|
|
8
|
+
puts "=== Animated Command Execution Example ==="
|
|
9
|
+
puts
|
|
10
|
+
|
|
11
|
+
# ejemplo simple con comandos secuenciales
|
|
12
|
+
runner.start("Running system checks", steps: 3)
|
|
13
|
+
|
|
14
|
+
runner.run("echo 'Checking disk space...' && sleep 1", aka: "disk check")
|
|
15
|
+
runner.run("echo 'Checking memory...' && sleep 1", aka: "memory check")
|
|
16
|
+
runner.run("echo 'Checking network...' && sleep 1", aka: "network check")
|
|
17
|
+
|
|
18
|
+
runner.stop
|
|
19
|
+
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
# ejemplo con comandos más complejos
|
|
23
|
+
runner.start("Installing dependencies", steps: 2)
|
|
24
|
+
|
|
25
|
+
runner.run("echo 'Fetching package list...' && sleep 2", aka: "apt update")
|
|
26
|
+
runner.run("echo 'Installing packages...' && sleep 2", aka: "apt install")
|
|
27
|
+
|
|
28
|
+
runner.stop
|
|
29
|
+
|
|
30
|
+
puts
|
|
31
|
+
|
|
32
|
+
# ejemplo con manejo de errores (comentado para no interrumpir)
|
|
33
|
+
# runner.start("Testing error handling")
|
|
34
|
+
# runner.run("false", aka: "failing command") # este comando fallará
|
|
35
|
+
# runner.stop
|
|
36
|
+
|
|
37
|
+
puts "=== End of Animated Examples ==="
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/starlined"
|
|
5
|
+
|
|
6
|
+
include Starlined::Messages
|
|
7
|
+
|
|
8
|
+
puts "=== Basic Message Examples ==="
|
|
9
|
+
puts
|
|
10
|
+
|
|
11
|
+
info("This is an information message")
|
|
12
|
+
sleep(1)
|
|
13
|
+
|
|
14
|
+
warn("This is a warning message")
|
|
15
|
+
sleep(1)
|
|
16
|
+
|
|
17
|
+
success("Operation completed successfully", 2.5)
|
|
18
|
+
sleep(1)
|
|
19
|
+
|
|
20
|
+
# configurar modo verbose
|
|
21
|
+
Starlined.configure do |config|
|
|
22
|
+
config.verbose = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
verbose("This verbose message is now visible")
|
|
26
|
+
sleep(1)
|
|
27
|
+
|
|
28
|
+
# ejemplo con error (comentado para no salir del programa)
|
|
29
|
+
# error("Something went wrong", "Connection timeout")
|
|
30
|
+
|
|
31
|
+
# entrada interactiva
|
|
32
|
+
name = ask("What's your name?")
|
|
33
|
+
info("Hello, #{name}!")
|
|
34
|
+
|
|
35
|
+
puts
|
|
36
|
+
puts "=== End of Basic Examples ==="
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/starlined"
|
|
5
|
+
|
|
6
|
+
puts "=== Custom Animation Example ==="
|
|
7
|
+
puts
|
|
8
|
+
|
|
9
|
+
# configuración personalizada
|
|
10
|
+
Starlined.configure do |config|
|
|
11
|
+
config.sleep_time = 0.3 # animación más rápida
|
|
12
|
+
config.stars_range = (0..8) # animación más ancha
|
|
13
|
+
config.msg_ljust = 35 # mensajes más largos
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# usar la animación directamente
|
|
17
|
+
animation = Starlined::Animation.new("Processing large dataset", steps: 10)
|
|
18
|
+
animation.start
|
|
19
|
+
|
|
20
|
+
10.times do |i|
|
|
21
|
+
# simular procesamiento de archivos
|
|
22
|
+
file_name = "data_chunk_#{i + 1}.csv"
|
|
23
|
+
animation.add_alias(file_name)
|
|
24
|
+
|
|
25
|
+
# simular trabajo
|
|
26
|
+
sleep(0.5 + rand * 0.5)
|
|
27
|
+
|
|
28
|
+
animation.increment_step
|
|
29
|
+
animation.remove_alias(file_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
animation.stop
|
|
33
|
+
|
|
34
|
+
# limpiar y mostrar resultado
|
|
35
|
+
include Starlined::Messages
|
|
36
|
+
success("Dataset processed", animation.elapsed_time)
|
|
37
|
+
|
|
38
|
+
puts
|
|
39
|
+
|
|
40
|
+
# otro ejemplo con múltiples aliases simultáneos
|
|
41
|
+
animation2 = Starlined::Animation.new("Downloading files", steps: 5)
|
|
42
|
+
animation2.start
|
|
43
|
+
|
|
44
|
+
# agregar múltiples aliases (simular descargas paralelas)
|
|
45
|
+
animation2.add_alias("file1.zip")
|
|
46
|
+
animation2.add_alias("file2.tar.gz")
|
|
47
|
+
animation2.add_alias("file3.json")
|
|
48
|
+
|
|
49
|
+
5.times do |i|
|
|
50
|
+
sleep(1)
|
|
51
|
+
animation2.increment_step
|
|
52
|
+
|
|
53
|
+
# eliminar un alias cuando "termina" la descarga
|
|
54
|
+
animation2.remove_alias("file#{i}.zip") if i < 3
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
animation2.stop
|
|
58
|
+
success("All files downloaded", animation2.elapsed_time)
|
|
59
|
+
|
|
60
|
+
puts
|
|
61
|
+
puts "=== End of Custom Animation Examples ==="
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../lib/starlined"
|
|
5
|
+
|
|
6
|
+
runner = Starlined::Runner.new
|
|
7
|
+
|
|
8
|
+
puts "=== Parallel Execution Example ==="
|
|
9
|
+
puts
|
|
10
|
+
|
|
11
|
+
runner.start("Running parallel tasks", steps: 4)
|
|
12
|
+
|
|
13
|
+
# crear múltiples hilos que ejecutan comandos en paralelo
|
|
14
|
+
threads = []
|
|
15
|
+
|
|
16
|
+
threads << Thread.new do
|
|
17
|
+
runner.run("echo 'Task 1 starting' && sleep 2 && echo 'Task 1 done'", aka: "task-1")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
threads << Thread.new do
|
|
21
|
+
runner.run("echo 'Task 2 starting' && sleep 3 && echo 'Task 2 done'", aka: "task-2")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
threads << Thread.new do
|
|
25
|
+
runner.run("echo 'Task 3 starting' && sleep 1 && echo 'Task 3 done'", aka: "task-3")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
threads << Thread.new do
|
|
29
|
+
runner.run("echo 'Task 4 starting' && sleep 2.5 && echo 'Task 4 done'", aka: "task-4")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# esperar a que todos los hilos terminen
|
|
33
|
+
threads.each(&:join)
|
|
34
|
+
|
|
35
|
+
runner.stop
|
|
36
|
+
|
|
37
|
+
puts
|
|
38
|
+
puts "All parallel tasks completed!"
|
|
39
|
+
puts
|
|
40
|
+
|
|
41
|
+
# ejemplo más realista: ejecutar diferentes tipos de tests en paralelo
|
|
42
|
+
runner.start("Running test suite", steps: 3)
|
|
43
|
+
|
|
44
|
+
test_threads = []
|
|
45
|
+
|
|
46
|
+
test_threads << Thread.new do
|
|
47
|
+
runner.run("echo 'Running unit tests...' && sleep 2", aka: "unit-tests")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
test_threads << Thread.new do
|
|
51
|
+
runner.run("echo 'Running integration tests...' && sleep 3", aka: "integration-tests")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
test_threads << Thread.new do
|
|
55
|
+
runner.run("echo 'Running linter...' && sleep 1.5", aka: "linter")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
test_threads.each(&:join)
|
|
59
|
+
|
|
60
|
+
runner.stop
|
|
61
|
+
|
|
62
|
+
puts
|
|
63
|
+
puts "=== End of Parallel Examples ==="
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
|
|
5
|
+
module Starlined
|
|
6
|
+
class Animation
|
|
7
|
+
attr_reader :thread, :message, :start_time, :steps, :current_steps
|
|
8
|
+
attr_accessor :aliases
|
|
9
|
+
|
|
10
|
+
def initialize(message = "Booting up", steps = 0)
|
|
11
|
+
@message = message
|
|
12
|
+
@start_time = Time.now
|
|
13
|
+
@steps = steps
|
|
14
|
+
@current_steps = 0
|
|
15
|
+
|
|
16
|
+
# variables de animación
|
|
17
|
+
@pos = [0, 1, 2]
|
|
18
|
+
@move = 1
|
|
19
|
+
|
|
20
|
+
# manejo de aliases
|
|
21
|
+
@aliases = []
|
|
22
|
+
@alias_pointer = 0
|
|
23
|
+
@alias_timer = 0
|
|
24
|
+
@alias_semaphore = Mutex.new
|
|
25
|
+
|
|
26
|
+
# semáforo para pasos
|
|
27
|
+
@steps_semaphore = Mutex.new
|
|
28
|
+
|
|
29
|
+
@running = false
|
|
30
|
+
@thread = nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start
|
|
34
|
+
return if @running
|
|
35
|
+
@running = true
|
|
36
|
+
@thread = Thread.new { animation_loop }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stop
|
|
40
|
+
return unless @running
|
|
41
|
+
@running = false
|
|
42
|
+
@thread&.kill
|
|
43
|
+
@thread = nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def increment_step
|
|
47
|
+
@steps_semaphore.synchronize do
|
|
48
|
+
@current_steps += 1 if @current_steps < @steps
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_alias(aka)
|
|
53
|
+
return if aka.nil?
|
|
54
|
+
@alias_semaphore.synchronize { @aliases.push(aka) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def remove_alias(aka)
|
|
58
|
+
return if aka.nil?
|
|
59
|
+
@alias_semaphore.synchronize do
|
|
60
|
+
@aliases.delete(aka)
|
|
61
|
+
# ajustar el puntero si se eliminó un elemento antes de la posición actual
|
|
62
|
+
@alias_pointer = 0 if @alias_pointer >= @aliases.length && @aliases.length > 0
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def elapsed_time
|
|
67
|
+
(Time.now - @start_time).round(2).to_s
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def animation_loop
|
|
73
|
+
config = Starlined.configuration
|
|
74
|
+
|
|
75
|
+
while @running
|
|
76
|
+
stars = build_stars
|
|
77
|
+
extra = build_extra_info
|
|
78
|
+
|
|
79
|
+
clear_line
|
|
80
|
+
print "\r[#{stars}] #{@message.ljust(config.msg_ljust)} #{extra}"
|
|
81
|
+
|
|
82
|
+
move_stars
|
|
83
|
+
update_alias_timer
|
|
84
|
+
|
|
85
|
+
sleep config.sleep_time
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_stars
|
|
90
|
+
stars = ""
|
|
91
|
+
config = Starlined.configuration
|
|
92
|
+
|
|
93
|
+
config.stars_range.each do |i|
|
|
94
|
+
if !@pos.include?(i)
|
|
95
|
+
stars += " "
|
|
96
|
+
elsif i == @pos[1]
|
|
97
|
+
stars += "*".bold.red
|
|
98
|
+
else
|
|
99
|
+
stars += "*".yellow
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
stars
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def build_extra_info
|
|
107
|
+
config = Starlined.configuration
|
|
108
|
+
extra = "#{elapsed_time}s".rjust(config.extra_rjust).light_black
|
|
109
|
+
|
|
110
|
+
if @steps != 0
|
|
111
|
+
steps_string = "#{@current_steps}/#{@steps}"
|
|
112
|
+
alias_string = current_alias_string
|
|
113
|
+
extra += " #{steps_string} #{alias_string.light_black.italic}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
extra
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def current_alias_string
|
|
120
|
+
@alias_semaphore.synchronize do
|
|
121
|
+
return "" if @aliases.empty?
|
|
122
|
+
|
|
123
|
+
begin
|
|
124
|
+
@aliases[@alias_pointer].to_s
|
|
125
|
+
rescue NoMethodError, ArgumentError
|
|
126
|
+
@aliases.first.to_s if @aliases.length > 0
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def move_stars
|
|
132
|
+
config = Starlined.configuration
|
|
133
|
+
|
|
134
|
+
# mover estrellas dependiendo del sentido del movimiento
|
|
135
|
+
@pos = @pos.map { |p| (p + @move) % config.stars_range.length }
|
|
136
|
+
|
|
137
|
+
# cambiar el sentido de movimiento si se toca alguno de los bordes
|
|
138
|
+
@move = @pos.first == config.stars_range.first ? 1 :
|
|
139
|
+
@pos.last == config.stars_range.last ? -1 : @move
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def update_alias_timer
|
|
143
|
+
@alias_semaphore.synchronize do
|
|
144
|
+
return if @aliases.empty?
|
|
145
|
+
|
|
146
|
+
@alias_timer += 1
|
|
147
|
+
if @alias_timer >= 3
|
|
148
|
+
@alias_timer = 0
|
|
149
|
+
@alias_pointer = (@alias_pointer + 1) % @aliases.length
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def clear_line
|
|
155
|
+
print "\r#{Starlined.configuration.clear_line_string}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Starlined
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :sleep_time, :msg_ljust, :extra_rjust, :stars_range,
|
|
6
|
+
:verbose, :clear_line_string
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@sleep_time = 0.75
|
|
10
|
+
@msg_ljust = 30
|
|
11
|
+
@extra_rjust = 8
|
|
12
|
+
@stars_range = (0..5).to_a
|
|
13
|
+
@verbose = false
|
|
14
|
+
@clear_line_string = "\33[2K"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
|
|
5
|
+
module Starlined
|
|
6
|
+
module Messages
|
|
7
|
+
extend self
|
|
8
|
+
|
|
9
|
+
def error(message = nil, context = nil)
|
|
10
|
+
clear_line
|
|
11
|
+
output = "\r[#{"FAILED".red}] #{context || "Operation failed"}"
|
|
12
|
+
output += " (#{message})" unless message.nil?
|
|
13
|
+
puts output
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def info(message)
|
|
17
|
+
clear_line
|
|
18
|
+
puts "\r[ #{"INFO".blue} ] #{message}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def warn(message)
|
|
22
|
+
clear_line
|
|
23
|
+
puts "\r[ #{"WARN".yellow} ] #{message}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def success(message, time = nil)
|
|
27
|
+
clear_line
|
|
28
|
+
output = "\r[ #{"OK".green} ] #{message}"
|
|
29
|
+
if time
|
|
30
|
+
dots = "." * [3, 36 - message.length - time.to_s.length].max
|
|
31
|
+
output += " #{dots.bold.gray} #{time}s"
|
|
32
|
+
end
|
|
33
|
+
puts output
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def verbose(message)
|
|
37
|
+
return unless Starlined.configuration.verbose
|
|
38
|
+
clear_line
|
|
39
|
+
puts "\r[#{"VRBOSE".light_black}] #{message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ask(prompt)
|
|
43
|
+
clear_line
|
|
44
|
+
print "\r[ #{"??".light_blue} ] #{prompt}: "
|
|
45
|
+
STDIN.gets.chomp
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def clear_line
|
|
51
|
+
print "\r#{Starlined.configuration.clear_line_string}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "colorize"
|
|
5
|
+
|
|
6
|
+
module Starlined
|
|
7
|
+
class Runner
|
|
8
|
+
attr_reader :animation
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@run_instances = 0
|
|
12
|
+
@run_semaphore = Mutex.new
|
|
13
|
+
@animation = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def start(message, steps: 0)
|
|
17
|
+
stop_animation if @animation
|
|
18
|
+
@animation = Animation.new(message, steps)
|
|
19
|
+
@animation
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def stop
|
|
23
|
+
return unless @animation
|
|
24
|
+
|
|
25
|
+
Messages.success(@animation.message, @animation.elapsed_time)
|
|
26
|
+
stop_animation
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run(command, print_err = true, aka: nil, no_count: false)
|
|
30
|
+
needs_sudo = !!(command =~ /^sudo/)
|
|
31
|
+
|
|
32
|
+
execute(
|
|
33
|
+
-> { Open3.capture3(command) },
|
|
34
|
+
print_err,
|
|
35
|
+
aka: aka,
|
|
36
|
+
no_count: no_count,
|
|
37
|
+
sudo: needs_sudo,
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def execute(callback, print_err = true, aka: nil, no_count: false, sudo: false)
|
|
42
|
+
handle_sudo if sudo
|
|
43
|
+
|
|
44
|
+
@run_semaphore.synchronize do
|
|
45
|
+
@animation&.add_alias(aka) unless aka.nil?
|
|
46
|
+
|
|
47
|
+
if @run_instances == 0 && @animation
|
|
48
|
+
@animation.start
|
|
49
|
+
end
|
|
50
|
+
@run_instances += 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
result = callback.call
|
|
54
|
+
|
|
55
|
+
@run_semaphore.synchronize do
|
|
56
|
+
@run_instances -= 1
|
|
57
|
+
@animation&.increment_step unless no_count
|
|
58
|
+
@animation&.remove_alias(aka) unless aka.nil?
|
|
59
|
+
|
|
60
|
+
if @run_instances == 0 && @animation
|
|
61
|
+
@animation.stop
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
handle_error(result, print_err) unless result.last.success?
|
|
66
|
+
|
|
67
|
+
result
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def stop_animation
|
|
73
|
+
@animation&.stop
|
|
74
|
+
@animation = nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def handle_sudo
|
|
78
|
+
needs_password = !system("sudo -n true &>/dev/null")
|
|
79
|
+
|
|
80
|
+
if needs_password || RUBY_PLATFORM.include?("darwin")
|
|
81
|
+
stop_animation
|
|
82
|
+
|
|
83
|
+
if needs_password
|
|
84
|
+
# alertar al usuario con diferentes métodos
|
|
85
|
+
print "\a" # terminal bell
|
|
86
|
+
system("tput bel 2>/dev/null") # alternative bell
|
|
87
|
+
Messages.info("Password required")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
result = system("sudo -v")
|
|
91
|
+
raise "Sudo privileges not granted" unless result
|
|
92
|
+
|
|
93
|
+
@animation&.start if @run_instances != 0
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def handle_error(result, print_err)
|
|
98
|
+
return unless print_err
|
|
99
|
+
|
|
100
|
+
stop_animation
|
|
101
|
+
Messages.error(nil, @animation&.message)
|
|
102
|
+
|
|
103
|
+
puts result[1] # stderr
|
|
104
|
+
puts result[0] if result[1].empty? # stdout si stderr está vacío
|
|
105
|
+
|
|
106
|
+
Messages.verbose("Exit code: #{result.last.exitstatus}")
|
|
107
|
+
exit 1
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
data/lib/starlined.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "colorize"
|
|
5
|
+
|
|
6
|
+
require_relative "starlined/version"
|
|
7
|
+
require_relative "starlined/configuration"
|
|
8
|
+
require_relative "starlined/animation"
|
|
9
|
+
require_relative "starlined/messages"
|
|
10
|
+
require_relative "starlined/runner"
|
|
11
|
+
|
|
12
|
+
module Starlined
|
|
13
|
+
class << self
|
|
14
|
+
attr_accessor :configuration
|
|
15
|
+
|
|
16
|
+
def configure
|
|
17
|
+
self.configuration ||= Configuration.new
|
|
18
|
+
yield(configuration) if block_given?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def reset_configuration!
|
|
22
|
+
self.configuration = Configuration.new
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# inicializar con configuración por defecto
|
|
27
|
+
self.configuration = Configuration.new
|
|
28
|
+
end
|
data/starlined.gemspec
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/starlined/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "starlined"
|
|
7
|
+
spec.version = Starlined::VERSION
|
|
8
|
+
spec.authors = ["mier"]
|
|
9
|
+
spec.email = ["ruby@mier.info"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Terminal output utilities with animated progress indicators"
|
|
12
|
+
spec.homepage = "https://github.com/miermontoto/starlined"
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
15
|
+
|
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
18
|
+
|
|
19
|
+
# especificar qué archivos deben incluirse en el gem
|
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
22
|
+
end
|
|
23
|
+
spec.bindir = "exe"
|
|
24
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
25
|
+
spec.require_paths = ["lib"]
|
|
26
|
+
|
|
27
|
+
# dependencias en tiempo de ejecución
|
|
28
|
+
spec.add_dependency "colorize", "~> 0.8"
|
|
29
|
+
|
|
30
|
+
# dependencias de desarrollo
|
|
31
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
32
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
33
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
|
34
|
+
spec.add_development_dependency "rubocop", "~> 1.0"
|
|
35
|
+
spec.add_development_dependency "simplecov", "~> 0.21"
|
|
36
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: starlined
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- mier
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: colorize
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.8'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.8'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: bundler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rubocop
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '1.0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '1.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: simplecov
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.21'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.21'
|
|
96
|
+
email:
|
|
97
|
+
- ruby@mier.info
|
|
98
|
+
executables: []
|
|
99
|
+
extensions: []
|
|
100
|
+
extra_rdoc_files: []
|
|
101
|
+
files:
|
|
102
|
+
- ".gitignore"
|
|
103
|
+
- Gemfile
|
|
104
|
+
- LICENSE.txt
|
|
105
|
+
- README.md
|
|
106
|
+
- Rakefile
|
|
107
|
+
- examples/animated_commands.rb
|
|
108
|
+
- examples/basic_messages.rb
|
|
109
|
+
- examples/custom_animation.rb
|
|
110
|
+
- examples/parallel_execution.rb
|
|
111
|
+
- lib/starlined.rb
|
|
112
|
+
- lib/starlined/animation.rb
|
|
113
|
+
- lib/starlined/configuration.rb
|
|
114
|
+
- lib/starlined/messages.rb
|
|
115
|
+
- lib/starlined/runner.rb
|
|
116
|
+
- lib/starlined/version.rb
|
|
117
|
+
- starlined.gemspec
|
|
118
|
+
homepage: https://github.com/miermontoto/starlined
|
|
119
|
+
licenses:
|
|
120
|
+
- MIT
|
|
121
|
+
metadata:
|
|
122
|
+
homepage_uri: https://github.com/miermontoto/starlined
|
|
123
|
+
source_code_uri: https://github.com/miermontoto/starlined
|
|
124
|
+
rdoc_options: []
|
|
125
|
+
require_paths:
|
|
126
|
+
- lib
|
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: 2.6.0
|
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - ">="
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '0'
|
|
137
|
+
requirements: []
|
|
138
|
+
rubygems_version: 3.6.9
|
|
139
|
+
specification_version: 4
|
|
140
|
+
summary: Terminal output utilities with animated progress indicators
|
|
141
|
+
test_files: []
|