philiprehberger-progress 0.3.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8cedfd9240c3ab6dd70848fbc63dca19ca4e3eea6d13ccada9e1663c73ca75d
4
- data.tar.gz: 3a81c872fc39dc070eaa68e0393c0e65d9878f7d54b01bd3ec9af557a40b92e2
3
+ metadata.gz: 8cd9b24ec4af77caa0242168972123e46f8aefe0aa7d010f355f634462d4e7ad
4
+ data.tar.gz: 956912f23898fd45c2434b06030aea103af6d33f390f11fc432a7cf285983325
5
5
  SHA512:
6
- metadata.gz: 309c39e3e947e5dc7c9465cd4f1a1feff1fa9ec38221a11325243202c8f9e6326f1e60b458138b9d7fb2ec50ddd5b98cdf7425227d4a913b0dda464a3fd3d137
7
- data.tar.gz: 985d65af32ab52d1310b2f01e24b7024a1595e1357a1f09c074ad9bc636ffb0445a33b00fc5a943472955cf5c0d663df516905cb67140db80734ed6af218300e
6
+ metadata.gz: 79fcad8d626d2869ac6c3bfa9d29ec243c134f7c140dfca896282c940bb82b3879d25378b229162441708d13fc204c6e912c5f802fef0b9331fc4bd9bb845889
7
+ data.tar.gz: d0074a78f1adfa22c1c749bf6367c468dfdfb1fa011d56ab88b83faedcc86625ada2f5982430cdc2f69013c66ead9bd030058d0b538959b3778cbfc94c86cb9c
data/CHANGELOG.md CHANGED
@@ -2,11 +2,29 @@
2
2
 
3
3
  All notable changes to this gem will be documented in this file.
4
4
 
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-04-14
11
+
12
+ ### Added
13
+ - `Bar#pause` and `Bar#resume` to freeze and unfreeze elapsed time calculation
14
+ - `Bar#to_h` returns a hash with `:percentage`, `:elapsed`, `:eta`, `:throughput`, `:current`, `:total`
15
+ - Custom bar characters via `fill:`, `empty:`, `tip:` keyword arguments (defaults: `fill: '='`, `empty: ' '`, `tip: '>'`)
16
+ - `Progress.json_mode!` and `Progress.text_mode!` to toggle JSON line output for bar rendering
17
+
18
+ ## [0.4.0] - 2026-04-09
19
+
20
+ ### Added
21
+ - `Bar#set(n)` to set absolute progress position
22
+ - `Bar#reset` to restart the bar from 0, preserving configuration
23
+ - `Spinner#message=` to update the spinner message dynamically
24
+
25
+ ### Changed
26
+ - Standardize CHANGELOG header format to match template
27
+
10
28
  ## [0.3.0] - 2026-04-09
11
29
 
12
30
  ### Added
@@ -92,17 +110,3 @@ and this gem adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
92
110
  - `Progress.spin` convenience method with block support
93
111
  - `Progress.each` for iterating enumerables with progress display
94
112
  - TTY detection to auto-disable rendering in non-terminal environments
95
-
96
- [0.3.0]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.3.0
97
- [0.2.0]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.2.0
98
- [0.1.11]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.11
99
- [0.1.10]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.10
100
- [0.1.9]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.9
101
- [0.1.8]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.8
102
- [0.1.7]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.7
103
- [0.1.6]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.6
104
- [0.1.5]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.5
105
- [0.1.4]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.4
106
- [0.1.3]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.3
107
- [0.1.2]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.2
108
- [0.1.0]: https://github.com/philiprehberger/rb-progress/releases/tag/v0.1.0
data/README.md CHANGED
@@ -81,6 +81,58 @@ end
81
81
 
82
82
  The thread is joined automatically when `stop` is called (or when the block completes).
83
83
 
84
+ ### Pause and Resume
85
+
86
+ Pause the progress bar to freeze elapsed time calculation (e.g. while waiting for user input):
87
+
88
+ ```ruby
89
+ bar = Philiprehberger::Progress::Bar.new(total: 100)
90
+ 50.times { bar.advance }
91
+ bar.pause
92
+ # ... elapsed time is frozen ...
93
+ bar.resume
94
+ 50.times { bar.advance }
95
+ bar.finish
96
+ ```
97
+
98
+ ### Custom Bar Characters
99
+
100
+ Customize the fill, empty, and tip characters:
101
+
102
+ ```ruby
103
+ bar = Philiprehberger::Progress::Bar.new(total: 100, fill: '#', empty: '.', tip: '>')
104
+ 50.times { bar.advance }
105
+ bar.to_s
106
+ # [########################>.........................] 50.0% | 50/100 | ETA: 0s | 50.0/s
107
+ ```
108
+
109
+ Default characters are `fill: '='`, `empty: ' '`, `tip: '>'`.
110
+
111
+ ### Data Export
112
+
113
+ Export the current state as a hash:
114
+
115
+ ```ruby
116
+ bar = Philiprehberger::Progress::Bar.new(total: 100)
117
+ 50.times { bar.advance }
118
+ bar.to_h
119
+ # => { percentage: 50.0, elapsed: 1.2, eta: 1.2, throughput: 41.7, current: 50, total: 100 }
120
+ ```
121
+
122
+ ### JSON Mode
123
+
124
+ Switch to JSON line output for machine-readable progress:
125
+
126
+ ```ruby
127
+ Philiprehberger::Progress.json_mode!
128
+ bar = Philiprehberger::Progress::Bar.new(total: 100)
129
+ 50.times { bar.advance }
130
+ bar.to_s
131
+ # {"percentage":50.0,"elapsed":1.2,"eta":1.2,"throughput":41.7,"current":50,"total":100}
132
+
133
+ Philiprehberger::Progress.text_mode! # revert to ANSI bar
134
+ ```
135
+
84
136
  ### Enumerable Integration
85
137
 
86
138
  ```ruby
@@ -121,21 +173,28 @@ multi.finished? # => true
121
173
 
122
174
  | Method | Description |
123
175
  |--------|-------------|
124
- | `.new(total:, width: 30, output: $stderr)` | Create a progress bar |
176
+ | `.new(total:, width: 30, output: $stderr, fill: '=', empty: ' ', tip: '>')` | Create a progress bar |
125
177
  | `#advance(n = 1)` | Advance by `n` items |
178
+ | `#set(n)` | Set absolute progress position (clamped to 0..total) |
179
+ | `#reset` | Reset to 0, clear finished state, restart timer |
180
+ | `#pause` | Pause the bar, freezing elapsed time |
181
+ | `#resume` | Resume after pause |
182
+ | `#paused?` | Whether the bar is paused |
126
183
  | `#finish` | Mark as complete |
127
184
  | `#finished?` | Whether the bar is finished |
128
185
  | `#percentage` | Current percentage (0.0 to 100.0) |
129
- | `#elapsed` | Elapsed time in seconds |
186
+ | `#elapsed` | Elapsed time in seconds (excludes paused time) |
130
187
  | `#eta` | Estimated time remaining in seconds |
131
188
  | `#throughput` | Items per second |
132
- | `#to_s` | Render the bar as a string |
189
+ | `#to_h` | Hash with `:percentage`, `:elapsed`, `:eta`, `:throughput`, `:current`, `:total` |
190
+ | `#to_s` | Render the bar as a string (or JSON line in json_mode) |
133
191
 
134
192
  ### `Philiprehberger::Progress::Spinner`
135
193
 
136
194
  | Method | Description |
137
195
  |--------|-------------|
138
196
  | `.new(message:, output: $stderr)` | Create a spinner |
197
+ | `#message=` | Update the spinner message dynamically |
139
198
  | `#spin` | Advance to the next frame |
140
199
  | `#auto_spin(interval: 0.1)` | Start background thread animation |
141
200
  | `#stop(final_message = 'done')` | Stop with a message (joins background thread) |
@@ -164,6 +223,9 @@ multi.finished? # => true
164
223
  | `Progress.multi(output: $stderr, &block)` | Create multi-bar tracker |
165
224
  | `Progress.each(enumerable, label: nil) { \|item\| }` | Iterate with progress |
166
225
  | `Progress.map(enumerable, label: nil) { \|item\| }` | Transform with progress, returns results |
226
+ | `Progress.json_mode!` | Switch bar rendering to JSON line output |
227
+ | `Progress.text_mode!` | Switch bar rendering back to ANSI text |
228
+ | `Progress.json_mode?` | Whether JSON mode is active |
167
229
 
168
230
  ## Development
169
231
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Philiprehberger
4
6
  module Progress
5
7
  class Bar
@@ -8,13 +10,19 @@ module Philiprehberger
8
10
 
9
11
  attr_reader :current, :total
10
12
 
11
- def initialize(total:, width: 30, output: $stderr)
13
+ def initialize(total:, width: 30, output: $stderr, fill: '=', empty: ' ', tip: '>')
12
14
  @total = [total, 0].max
13
15
  @width = width
14
16
  @output = output
15
17
  @current = 0
16
18
  @start_time = now
17
19
  @finished = false
20
+ @paused = false
21
+ @pause_elapsed = 0.0
22
+ @pause_start = nil
23
+ @fill = fill
24
+ @empty = empty
25
+ @tip = tip
18
26
  end
19
27
 
20
28
  def advance(n = 1)
@@ -25,6 +33,58 @@ module Philiprehberger
25
33
  self
26
34
  end
27
35
 
36
+ # Set absolute progress position.
37
+ #
38
+ # @param n [Integer] the new current value (clamped to 0..total)
39
+ # @return [self]
40
+ def set(n)
41
+ return self if @finished
42
+
43
+ @current = [[n, 0].max, @total].min
44
+ render_to_output
45
+ self
46
+ end
47
+
48
+ # Reset the bar to 0, preserving total and width. Restarts the timer.
49
+ #
50
+ # @return [self]
51
+ def reset
52
+ @current = 0
53
+ @finished = false
54
+ @paused = false
55
+ @pause_elapsed = 0.0
56
+ @pause_start = nil
57
+ @start_time = now
58
+ self
59
+ end
60
+
61
+ # Pause the progress bar, freezing elapsed time calculation.
62
+ #
63
+ # @return [self]
64
+ def pause
65
+ return self if @paused || @finished
66
+
67
+ @paused = true
68
+ @pause_start = now
69
+ self
70
+ end
71
+
72
+ # Resume after pause.
73
+ #
74
+ # @return [self]
75
+ def resume
76
+ return self unless @paused
77
+
78
+ @pause_elapsed += now - @pause_start
79
+ @pause_start = nil
80
+ @paused = false
81
+ self
82
+ end
83
+
84
+ def paused?
85
+ @paused
86
+ end
87
+
28
88
  def finish
29
89
  @current = @total
30
90
  @finished = true
@@ -44,7 +104,9 @@ module Philiprehberger
44
104
  end
45
105
 
46
106
  def elapsed
47
- now - @start_time
107
+ raw = now - @start_time - @pause_elapsed
108
+ raw -= (now - @pause_start) if @paused
109
+ raw
48
110
  end
49
111
 
50
112
  def eta
@@ -63,7 +125,20 @@ module Philiprehberger
63
125
  @current.to_f / elapsed_time
64
126
  end
65
127
 
128
+ def to_h
129
+ {
130
+ percentage: percentage,
131
+ elapsed: elapsed,
132
+ eta: eta,
133
+ throughput: throughput,
134
+ current: @current,
135
+ total: @total
136
+ }
137
+ end
138
+
66
139
  def to_s
140
+ return to_h.to_json if Philiprehberger::Progress.json_mode?
141
+
67
142
  bar_str = render_bar
68
143
  pct = format('%<p>5.1f%%', p: percentage)
69
144
  eta_str = format_eta(eta)
@@ -87,11 +162,16 @@ module Philiprehberger
87
162
  end
88
163
 
89
164
  def render_bar
90
- return FILL_CHAR * @width if @total.zero?
165
+ return @fill * @width if @total.zero?
91
166
 
92
167
  filled = (@current.to_f / @total * @width).round
93
168
  empty = @width - filled
94
- (FILL_CHAR * filled) + (EMPTY_CHAR * empty)
169
+
170
+ if filled.positive? && filled < @width
171
+ (@fill * (filled - 1)) + @tip + (@empty * empty)
172
+ else
173
+ (@fill * filled) + (@empty * empty)
174
+ end
95
175
  end
96
176
 
97
177
  def format_eta(seconds)
@@ -6,7 +6,7 @@ module Philiprehberger
6
6
  FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807",
7
7
  "\u280F"].freeze
8
8
 
9
- attr_reader :message
9
+ attr_accessor :message
10
10
 
11
11
  def initialize(message:, output: $stderr)
12
12
  @message = message
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module Progress
5
- VERSION = '0.3.0'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -7,8 +7,22 @@ require_relative 'progress/multi'
7
7
 
8
8
  module Philiprehberger
9
9
  module Progress
10
- def self.bar(total:, width: 30, output: $stderr)
11
- progress_bar = Bar.new(total: total, width: width, output: output)
10
+ @json_mode = false
11
+
12
+ def self.json_mode!
13
+ @json_mode = true
14
+ end
15
+
16
+ def self.text_mode!
17
+ @json_mode = false
18
+ end
19
+
20
+ def self.json_mode?
21
+ @json_mode
22
+ end
23
+
24
+ def self.bar(total:, width: 30, output: $stderr, fill: '=', empty: ' ', tip: '>')
25
+ progress_bar = Bar.new(total: total, width: width, output: output, fill: fill, empty: empty, tip: tip)
12
26
 
13
27
  if block_given?
14
28
  result = yield progress_bar
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-progress
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-10 00:00:00.000000000 Z
11
+ date: 2026-04-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Display progress bars with percentage, ETA, and throughput, or spinners
14
14
  for indeterminate tasks. Supports block-based usage, enumerable iteration with each/map,