philiprehberger-progress 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/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +140 -0
- data/lib/philiprehberger/progress/bar.rb +154 -0
- data/lib/philiprehberger/progress/multi.rb +70 -0
- data/lib/philiprehberger/progress/spinner.rb +89 -0
- data/lib/philiprehberger/progress/version.rb +7 -0
- data/lib/philiprehberger/progress.rb +86 -0
- metadata +58 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ba10a34300c085657e70aed11c006694d2c0ea2dccba5e0ea82a6c04fce1c50f
|
|
4
|
+
data.tar.gz: 78f0c3f6adb6c673b841dffb3e6743ac44042de833b8bf7e7bb1a75132e827a8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 17a190189ef3f1fb0487ba312796067c4fc91599452698eb7cb390fc1c928e14f9c5dce0b81d3107dd3c56c2af1fcbd4336c949db896b7d680b74f71dd65ed61
|
|
7
|
+
data.tar.gz: b7d8033d7265d4719a60b455b9635047ca0adbdf72eacb96445a7a92960a88608ac0375ebe3d2481075a1cc7bb65417db5869becbbf43882817728c3146b3a54
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- `Bar` class with percentage, ETA, throughput, and customizable format
|
|
15
|
+
- `Spinner` class with multiple frame sets (default, braille, dots)
|
|
16
|
+
- `Multi` class for tracking multiple concurrent progress bars
|
|
17
|
+
- Convenience methods `Progress.bar`, `Progress.spin`, `Progress.multi`
|
|
18
|
+
- `Enumerable#each_with_progress` integration
|
|
19
|
+
- TTY detection to auto-disable rendering in non-terminal environments
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philiprehberger
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# philiprehberger-progress
|
|
2
|
+
|
|
3
|
+
[](https://github.com/philiprehberger/rb-progress/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/philiprehberger-progress)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Terminal progress bars and spinners with ETA calculation and throughput display
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby >= 3.1
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem 'philiprehberger-progress'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install philiprehberger-progress
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Progress Bar
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'philiprehberger/progress'
|
|
33
|
+
|
|
34
|
+
Philiprehberger::Progress.bar(total: 100) do |bar|
|
|
35
|
+
100.times do
|
|
36
|
+
sleep(0.01)
|
|
37
|
+
bar.advance
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Custom Format
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
bar = Philiprehberger::Progress::Bar.new(
|
|
46
|
+
total: 100,
|
|
47
|
+
format: ':bar :percent | :current/:total | :rate items/s | ETA: :eta',
|
|
48
|
+
width: 30
|
|
49
|
+
)
|
|
50
|
+
bar.advance(50)
|
|
51
|
+
puts bar.to_s
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Spinner
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Philiprehberger::Progress.spin('Loading...') do |spinner|
|
|
58
|
+
10.times do
|
|
59
|
+
sleep(0.1)
|
|
60
|
+
spinner.spin
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Enumerable Integration
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
items = (1..100).to_a
|
|
69
|
+
items.each_with_progress('Processing') do |item|
|
|
70
|
+
sleep(0.01)
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Multi-Bar
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
multi = Philiprehberger::Progress.multi
|
|
78
|
+
bar1 = multi.bar('Downloads', total: 100)
|
|
79
|
+
bar2 = multi.bar('Uploads', total: 50)
|
|
80
|
+
|
|
81
|
+
bar1.advance(10)
|
|
82
|
+
bar2.advance(5)
|
|
83
|
+
multi.render
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
### `Philiprehberger::Progress`
|
|
89
|
+
|
|
90
|
+
| Method | Description |
|
|
91
|
+
|--------|-------------|
|
|
92
|
+
| `.bar(total:, format:, width:)` | Create a progress bar (yields if block given) |
|
|
93
|
+
| `.spin(message, frames:)` | Create a spinner (yields if block given) |
|
|
94
|
+
| `.multi` | Create a multi-bar display |
|
|
95
|
+
|
|
96
|
+
### `Philiprehberger::Progress::Bar`
|
|
97
|
+
|
|
98
|
+
| Method | Description |
|
|
99
|
+
|--------|-------------|
|
|
100
|
+
| `.new(total:, format:, width:)` | Create a progress bar |
|
|
101
|
+
| `#advance(n)` | Advance by `n` items (default: 1) |
|
|
102
|
+
| `#finish` | Mark as complete |
|
|
103
|
+
| `#finished?` | Whether the bar is finished |
|
|
104
|
+
| `#percentage` | Current percentage (0.0 to 100.0) |
|
|
105
|
+
| `#elapsed` | Elapsed time in seconds |
|
|
106
|
+
| `#eta` | Estimated time remaining in seconds |
|
|
107
|
+
| `#rate` | Throughput in items per second |
|
|
108
|
+
| `#to_s` | Render the bar as a string |
|
|
109
|
+
|
|
110
|
+
### `Philiprehberger::Progress::Spinner`
|
|
111
|
+
|
|
112
|
+
| Method | Description |
|
|
113
|
+
|--------|-------------|
|
|
114
|
+
| `.new(message:, frames:)` | Create a spinner |
|
|
115
|
+
| `#spin` | Advance to the next frame |
|
|
116
|
+
| `#done(message)` | Mark as done with optional message |
|
|
117
|
+
| `#done?` | Whether the spinner is done |
|
|
118
|
+
| `#to_s` | Render the current frame |
|
|
119
|
+
|
|
120
|
+
### `Philiprehberger::Progress::Multi`
|
|
121
|
+
|
|
122
|
+
| Method | Description |
|
|
123
|
+
|--------|-------------|
|
|
124
|
+
| `.new` | Create a multi-bar display |
|
|
125
|
+
| `#bar(label, total:)` | Add a new progress bar |
|
|
126
|
+
| `#render` | Render all bars |
|
|
127
|
+
| `#size` | Number of bars |
|
|
128
|
+
| `#finished?` | Whether all bars are finished |
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
bundle install
|
|
134
|
+
bundle exec rspec # Run tests
|
|
135
|
+
bundle exec rubocop # Check code style
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module Progress
|
|
5
|
+
# A terminal progress bar with ETA and throughput display
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# bar = Bar.new(total: 100)
|
|
9
|
+
# 100.times { bar.advance }
|
|
10
|
+
# bar.finish
|
|
11
|
+
class Bar
|
|
12
|
+
DEFAULT_FORMAT = ':bar :percent | :current/:total | :rate items/s | ETA: :eta'
|
|
13
|
+
DEFAULT_WIDTH = 30
|
|
14
|
+
FILL_CHAR = "\u2588"
|
|
15
|
+
EMPTY_CHAR = "\u2591"
|
|
16
|
+
|
|
17
|
+
attr_reader :current, :total
|
|
18
|
+
|
|
19
|
+
# Create a new progress bar
|
|
20
|
+
#
|
|
21
|
+
# @param total [Integer] total number of items
|
|
22
|
+
# @param format [String] format string with placeholders
|
|
23
|
+
# @param width [Integer] width of the bar portion in characters
|
|
24
|
+
# @param output [IO] output stream (default: $stderr)
|
|
25
|
+
def initialize(total:, format: DEFAULT_FORMAT, width: DEFAULT_WIDTH, output: $stderr)
|
|
26
|
+
@total = [total, 0].max
|
|
27
|
+
@format = format
|
|
28
|
+
@width = width
|
|
29
|
+
@output = output
|
|
30
|
+
@current = 0
|
|
31
|
+
@start_time = now
|
|
32
|
+
@finished = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Advance the progress bar
|
|
36
|
+
#
|
|
37
|
+
# @param n [Integer] number of items to advance (default: 1)
|
|
38
|
+
# @return [self]
|
|
39
|
+
def advance(n = 1)
|
|
40
|
+
return self if @finished
|
|
41
|
+
|
|
42
|
+
@current = [@current + n, @total].min
|
|
43
|
+
render if tty?
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Mark the progress as complete
|
|
48
|
+
#
|
|
49
|
+
# @return [self]
|
|
50
|
+
def finish
|
|
51
|
+
@current = @total
|
|
52
|
+
@finished = true
|
|
53
|
+
render if tty?
|
|
54
|
+
@output.write("\n") if tty?
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if the progress is finished
|
|
59
|
+
#
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
def finished?
|
|
62
|
+
@finished
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get the progress percentage
|
|
66
|
+
#
|
|
67
|
+
# @return [Float] percentage from 0.0 to 100.0
|
|
68
|
+
def percentage
|
|
69
|
+
return 100.0 if @total.zero?
|
|
70
|
+
|
|
71
|
+
(@current.to_f / @total * 100).round(1)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Get the elapsed time in seconds
|
|
75
|
+
#
|
|
76
|
+
# @return [Float]
|
|
77
|
+
def elapsed
|
|
78
|
+
now - @start_time
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get the estimated time remaining in seconds
|
|
82
|
+
#
|
|
83
|
+
# @return [Float, nil] nil if no progress has been made
|
|
84
|
+
def eta
|
|
85
|
+
return 0.0 if @current >= @total
|
|
86
|
+
return nil if @current.zero?
|
|
87
|
+
|
|
88
|
+
elapsed_time = elapsed
|
|
89
|
+
rate = @current.to_f / elapsed_time
|
|
90
|
+
(@total - @current) / rate
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get the throughput in items per second
|
|
94
|
+
#
|
|
95
|
+
# @return [Float]
|
|
96
|
+
def rate
|
|
97
|
+
elapsed_time = elapsed
|
|
98
|
+
return 0.0 if elapsed_time.zero?
|
|
99
|
+
|
|
100
|
+
@current.to_f / elapsed_time
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Render the progress bar as a string
|
|
104
|
+
#
|
|
105
|
+
# @return [String]
|
|
106
|
+
def to_s
|
|
107
|
+
result = @format.dup
|
|
108
|
+
result.gsub!(':bar', render_bar)
|
|
109
|
+
result.gsub!(':percent', format('%<pct>5.1f%%', pct: percentage))
|
|
110
|
+
result.gsub!(':current', @current.to_s)
|
|
111
|
+
result.gsub!(':total', @total.to_s)
|
|
112
|
+
result.gsub!(':rate', format('%<r>.1f', r: rate))
|
|
113
|
+
result.gsub!(':eta', format_duration(eta))
|
|
114
|
+
result.gsub!(':elapsed', format_duration(elapsed))
|
|
115
|
+
result
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def now
|
|
121
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def tty?
|
|
125
|
+
@output.respond_to?(:tty?) && @output.tty?
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def render
|
|
129
|
+
@output.write("\r#{self}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def render_bar
|
|
133
|
+
return FILL_CHAR * @width if @total.zero?
|
|
134
|
+
|
|
135
|
+
filled = (@current.to_f / @total * @width).round
|
|
136
|
+
empty = @width - filled
|
|
137
|
+
(FILL_CHAR * filled) + (EMPTY_CHAR * empty)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def format_duration(seconds)
|
|
141
|
+
return '--:--' if seconds.nil? || seconds.negative?
|
|
142
|
+
|
|
143
|
+
seconds = seconds.to_i
|
|
144
|
+
if seconds < 60
|
|
145
|
+
format('0:%02d', seconds)
|
|
146
|
+
elsif seconds < 3600
|
|
147
|
+
format('%d:%02d', seconds / 60, seconds % 60)
|
|
148
|
+
else
|
|
149
|
+
format('%d:%02d:%02d', seconds / 3600, (seconds % 3600) / 60, seconds % 60)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module Progress
|
|
5
|
+
# Multi-bar progress display for tracking multiple concurrent tasks
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# multi = Multi.new
|
|
9
|
+
# bar1 = multi.bar('Downloads', total: 100)
|
|
10
|
+
# bar2 = multi.bar('Uploads', total: 50)
|
|
11
|
+
class Multi
|
|
12
|
+
# Create a new multi-bar display
|
|
13
|
+
#
|
|
14
|
+
# @param output [IO] output stream (default: $stderr)
|
|
15
|
+
def initialize(output: $stderr)
|
|
16
|
+
@output = output
|
|
17
|
+
@bars = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Add a new progress bar
|
|
21
|
+
#
|
|
22
|
+
# @param label [String] label for the bar
|
|
23
|
+
# @param total [Integer] total items
|
|
24
|
+
# @param format [String] format string
|
|
25
|
+
# @param width [Integer] bar width
|
|
26
|
+
# @return [Bar]
|
|
27
|
+
def bar(label, total:, format: nil, width: 20)
|
|
28
|
+
bar_format = format || ":bar :percent | #{label}"
|
|
29
|
+
progress_bar = Bar.new(total: total, format: bar_format, width: width, output: StringIO.new)
|
|
30
|
+
@bars << { label: label, bar: progress_bar }
|
|
31
|
+
progress_bar
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Render all bars
|
|
35
|
+
#
|
|
36
|
+
# @return [self]
|
|
37
|
+
def render
|
|
38
|
+
return self unless tty?
|
|
39
|
+
|
|
40
|
+
# Move cursor up to overwrite previous render
|
|
41
|
+
@output.write("\e[#{@bars.length}A") if @rendered_once
|
|
42
|
+
@bars.each do |entry|
|
|
43
|
+
@output.write("\r\e[2K#{entry[:bar]}\n")
|
|
44
|
+
end
|
|
45
|
+
@rendered_once = true
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Number of bars being tracked
|
|
50
|
+
#
|
|
51
|
+
# @return [Integer]
|
|
52
|
+
def size
|
|
53
|
+
@bars.length
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check if all bars are finished
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def finished?
|
|
60
|
+
@bars.all? { |entry| entry[:bar].finished? }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def tty?
|
|
66
|
+
@output.respond_to?(:tty?) && @output.tty?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module Progress
|
|
5
|
+
# A terminal spinner for indeterminate progress
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# spinner = Spinner.new(message: 'Loading...')
|
|
9
|
+
# spinner.spin
|
|
10
|
+
# spinner.done
|
|
11
|
+
class Spinner
|
|
12
|
+
DEFAULT_FRAMES = %w[| / - \\].freeze
|
|
13
|
+
BRAILLE_FRAMES = %W[\u2807 \u2816 \u2830 \u2821 \u280B \u2819 \u2838 \u2824].freeze
|
|
14
|
+
DOTS_FRAMES = %W[\u2800 \u2801 \u2803 \u2807 \u280F \u281F \u283F \u287F \u28FF].freeze
|
|
15
|
+
|
|
16
|
+
FRAME_SETS = {
|
|
17
|
+
default: DEFAULT_FRAMES,
|
|
18
|
+
braille: BRAILLE_FRAMES,
|
|
19
|
+
dots: DOTS_FRAMES
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Create a new spinner
|
|
23
|
+
#
|
|
24
|
+
# @param message [String] message to display next to the spinner
|
|
25
|
+
# @param frames [Symbol, Array<String>] frame set name or custom frames
|
|
26
|
+
# @param output [IO] output stream (default: $stderr)
|
|
27
|
+
def initialize(message: '', frames: :default, output: $stderr)
|
|
28
|
+
@message = message
|
|
29
|
+
@frames = frames.is_a?(Symbol) ? FRAME_SETS.fetch(frames, DEFAULT_FRAMES) : frames
|
|
30
|
+
@output = output
|
|
31
|
+
@index = 0
|
|
32
|
+
@done = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Advance the spinner by one frame
|
|
36
|
+
#
|
|
37
|
+
# @return [self]
|
|
38
|
+
def spin
|
|
39
|
+
return self if @done
|
|
40
|
+
|
|
41
|
+
render if tty?
|
|
42
|
+
@index = (@index + 1) % @frames.length
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Mark the spinner as done
|
|
47
|
+
#
|
|
48
|
+
# @param message [String] optional completion message
|
|
49
|
+
# @return [self]
|
|
50
|
+
def done(message = nil)
|
|
51
|
+
@done = true
|
|
52
|
+
if tty?
|
|
53
|
+
clear_line
|
|
54
|
+
@output.write("#{message || @message}\n") if message || !@message.empty?
|
|
55
|
+
end
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if the spinner is done
|
|
60
|
+
#
|
|
61
|
+
# @return [Boolean]
|
|
62
|
+
def done?
|
|
63
|
+
@done
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Return the current frame
|
|
67
|
+
#
|
|
68
|
+
# @return [String]
|
|
69
|
+
def to_s
|
|
70
|
+
frame = @frames[@index % @frames.length]
|
|
71
|
+
@message.empty? ? frame : "#{frame} #{@message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def tty?
|
|
77
|
+
@output.respond_to?(:tty?) && @output.tty?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render
|
|
81
|
+
@output.write("\r#{self}")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def clear_line
|
|
85
|
+
@output.write("\r\e[2K")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
require_relative 'progress/version'
|
|
5
|
+
require_relative 'progress/bar'
|
|
6
|
+
require_relative 'progress/spinner'
|
|
7
|
+
require_relative 'progress/multi'
|
|
8
|
+
|
|
9
|
+
module Philiprehberger
|
|
10
|
+
module Progress
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Create and yield a progress bar
|
|
14
|
+
#
|
|
15
|
+
# @param total [Integer] total number of items
|
|
16
|
+
# @param format [String] format string
|
|
17
|
+
# @param width [Integer] bar width
|
|
18
|
+
# @param output [IO] output stream
|
|
19
|
+
# @yield [Bar] the progress bar
|
|
20
|
+
# @return [Object] the block's return value
|
|
21
|
+
def self.bar(total:, format: Bar::DEFAULT_FORMAT, width: Bar::DEFAULT_WIDTH, output: $stderr, &block)
|
|
22
|
+
progress_bar = Bar.new(total: total, format: format, width: width, output: output)
|
|
23
|
+
|
|
24
|
+
if block
|
|
25
|
+
result = yield progress_bar
|
|
26
|
+
progress_bar.finish unless progress_bar.finished?
|
|
27
|
+
result
|
|
28
|
+
else
|
|
29
|
+
progress_bar
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Create and yield a spinner
|
|
34
|
+
#
|
|
35
|
+
# @param message [String] message to display
|
|
36
|
+
# @param frames [Symbol, Array<String>] frame set
|
|
37
|
+
# @param output [IO] output stream
|
|
38
|
+
# @yield the work to perform
|
|
39
|
+
# @return [Object] the block's return value
|
|
40
|
+
def self.spin(message = 'Loading...', frames: :default, output: $stderr, &block)
|
|
41
|
+
spinner = Spinner.new(message: message, frames: frames, output: output)
|
|
42
|
+
|
|
43
|
+
if block
|
|
44
|
+
result = yield spinner
|
|
45
|
+
spinner.done unless spinner.done?
|
|
46
|
+
result
|
|
47
|
+
else
|
|
48
|
+
spinner
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Create a multi-bar display
|
|
53
|
+
#
|
|
54
|
+
# @param output [IO] output stream
|
|
55
|
+
# @return [Multi]
|
|
56
|
+
def self.multi(output: $stderr)
|
|
57
|
+
Multi.new(output: output)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Enumerable integration
|
|
63
|
+
module Enumerable
|
|
64
|
+
# Iterate with a progress bar
|
|
65
|
+
#
|
|
66
|
+
# @param message [String] label for the progress bar
|
|
67
|
+
# @param output [IO] output stream
|
|
68
|
+
# @yield [Object] each element
|
|
69
|
+
# @return [Array]
|
|
70
|
+
def each_with_progress(message = 'Processing', output: $stderr)
|
|
71
|
+
items = to_a
|
|
72
|
+
bar = Philiprehberger::Progress::Bar.new(
|
|
73
|
+
total: items.length,
|
|
74
|
+
format: ":bar :percent | #{message} | :current/:total",
|
|
75
|
+
output: output
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
items.each do |item|
|
|
79
|
+
yield item
|
|
80
|
+
bar.advance
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
bar.finish
|
|
84
|
+
items
|
|
85
|
+
end
|
|
86
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: philiprehberger-progress
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Philip Rehberger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Display progress bars with percentage, ETA, and throughput, or spinners
|
|
14
|
+
for indeterminate tasks. Supports multi-bar display, Enumerable integration, and
|
|
15
|
+
auto-disables when not connected to a terminal.
|
|
16
|
+
email:
|
|
17
|
+
- me@philiprehberger.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- lib/philiprehberger/progress.rb
|
|
26
|
+
- lib/philiprehberger/progress/bar.rb
|
|
27
|
+
- lib/philiprehberger/progress/multi.rb
|
|
28
|
+
- lib/philiprehberger/progress/spinner.rb
|
|
29
|
+
- lib/philiprehberger/progress/version.rb
|
|
30
|
+
homepage: https://github.com/philiprehberger/rb-progress
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata:
|
|
34
|
+
homepage_uri: https://github.com/philiprehberger/rb-progress
|
|
35
|
+
source_code_uri: https://github.com/philiprehberger/rb-progress
|
|
36
|
+
changelog_uri: https://github.com/philiprehberger/rb-progress/blob/main/CHANGELOG.md
|
|
37
|
+
bug_tracker_uri: https://github.com/philiprehberger/rb-progress/issues
|
|
38
|
+
rubygems_mfa_required: 'true'
|
|
39
|
+
post_install_message:
|
|
40
|
+
rdoc_options: []
|
|
41
|
+
require_paths:
|
|
42
|
+
- lib
|
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: 3.1.0
|
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
requirements: []
|
|
54
|
+
rubygems_version: 3.5.22
|
|
55
|
+
signing_key:
|
|
56
|
+
specification_version: 4
|
|
57
|
+
summary: Terminal progress bars and spinners with ETA calculation and throughput display
|
|
58
|
+
test_files: []
|