whirly 0.1.1 → 0.2.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.
@@ -0,0 +1,86 @@
1
+ {
2
+ "roman_numerals": {
3
+ "interval": 90,
4
+ "mode": "swing",
5
+ "frames": [
6
+ "Ⅰ",
7
+ "Ⅱ",
8
+ "Ⅲ",
9
+ "Ⅳ",
10
+ "Ⅴ",
11
+ "Ⅵ",
12
+ "Ⅶ",
13
+ "Ⅷ",
14
+ "Ⅸ",
15
+ "Ⅹ"
16
+ ]
17
+ },
18
+ "double_mark": {
19
+ "interval": 120,
20
+ "mode": "random",
21
+ "frames": [
22
+ "⁇",
23
+ "⁈",
24
+ "⁉",
25
+ "‼"
26
+ ]
27
+ },
28
+ "heart_exclamation": {
29
+ "interval": 45,
30
+ "frames": [
31
+ "❢",
32
+ "❣"
33
+ ]
34
+ },
35
+ "pencil": {
36
+ "interval": 200,
37
+ "frames": [
38
+ "✏",
39
+ "✎"
40
+ ]
41
+ },
42
+ "bars": {
43
+ "interval": 80,
44
+ "mode": "swing",
45
+ "frames": [
46
+ "𝍠",
47
+ "𝍡",
48
+ "𝍢",
49
+ "𝍣",
50
+ "𝍤"
51
+ ]
52
+ },
53
+ "dice": {
54
+ "interval": 100,
55
+ "mode": "random",
56
+ "frames": [
57
+ "⚀",
58
+ "⚁",
59
+ "⚂",
60
+ "⚃",
61
+ "⚄",
62
+ "⚅"
63
+ ]
64
+ },
65
+ "hanoi": {
66
+ "interval": 150,
67
+ "mode": "swing",
68
+ "frames": [
69
+ "𝍥",
70
+ "𝍦",
71
+ "𝍧",
72
+ "𝍨"
73
+ ]
74
+ },
75
+ "vertical_bars": {
76
+ "interval": 80,
77
+ "mode": "swing",
78
+ "frames": [
79
+ "𝍩",
80
+ "𝍪",
81
+ "𝍫",
82
+ "𝍬",
83
+ "𝍭"
84
+ ]
85
+ }
86
+ }
@@ -0,0 +1,21 @@
1
+ require_relative "../lib/whirly"
2
+ require "paint"
3
+
4
+ # Demonstrates all available spinners
5
+
6
+ if spinner_pack = $*[0]
7
+ constants = [spinner_pack.upcase]
8
+ else
9
+ constants = Whirly::Spinners.constants
10
+ end
11
+
12
+ constants.each{ |spinner_pack|
13
+ puts
14
+ puts Paint[spinner_pack, :underline]
15
+ puts
16
+ Whirly::Spinners.const_get(spinner_pack).keys.sort.each{ |spinner_name|
17
+ Whirly.start(spinner: spinner_name, status: spinner_name){
18
+ sleep 1.5
19
+ }
20
+ }
21
+ }
@@ -0,0 +1,12 @@
1
+ require_relative "../lib/whirly"
2
+ require "paint"
3
+
4
+ system "clear"
5
+
6
+ Whirly::Spinners::WHIRLY.keys.sort.each{ |spinner_name|
7
+ Whirly.start(spinner: spinner_name, status: spinner_name, append_newline: false, ansi_escape_mode: "line", remove_after_stop: true){
8
+ sleep 1.5
9
+ }
10
+ }
11
+
12
+ system "exit"
@@ -1,6 +1,8 @@
1
- require_relative 'lib/whirly'
1
+ require_relative '../lib/whirly'
2
2
  require 'paint'
3
3
 
4
+ # Lightning talk at EuRuKo 2016
5
+
4
6
  # # # Whirly
5
7
 
6
8
  print "\033c"
@@ -22,16 +24,6 @@ puts
22
24
  puts "Done"
23
25
  sleep 5
24
26
 
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
27
  # # # Earth
36
28
 
37
29
  print "\033c"
@@ -48,14 +40,6 @@ puts
48
40
  puts "Done"
49
41
  sleep 5
50
42
 
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
43
  # # # Pong Game
60
44
 
61
45
  print "\033c"
@@ -65,22 +49,12 @@ Whirly.start spinner: "pong", use_color: false, status: "Two computers in a game
65
49
  sleep 9
66
50
  end
67
51
 
68
-
69
52
  puts
70
53
  puts
71
54
  puts
72
55
  puts "Done"
73
56
  sleep 5
74
57
 
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
58
  # # # Ticking Clock
85
59
 
86
60
  print "\033c"
@@ -95,23 +69,11 @@ puts
95
69
  puts
96
70
  puts "Time is over"
97
71
 
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
72
  # # # URL
109
73
 
110
-
111
74
  print "\033c"
112
75
  puts Paint["Get WHIRLY", :bold]
113
76
 
114
- Whirly.start status: 'https://github.com/janlelis/whirly' do
77
+ Whirly.start spinner: "whirly", status: "https://github.com/janlelis/whirly" do
115
78
  sleep 60
116
79
  end
117
-
@@ -0,0 +1,20 @@
1
+ require_relative "../lib/whirly"
2
+ require "paint"
3
+
4
+ # Demonstrate the look of using multiple spinners
5
+
6
+ Whirly.configure(spinner: "dots", stop: "✔")
7
+
8
+ Whirly.start status: "Processing" do
9
+ sleep 2
10
+ end
11
+
12
+ Whirly.start status: "More processing" do
13
+ sleep 2
14
+ end
15
+
16
+ Whirly.start status: "Even more processing" do
17
+ sleep 2
18
+ end
19
+
20
+ puts "Done"
@@ -0,0 +1,6 @@
1
+ require_relative "../lib/whirly"
2
+ require "paint"
3
+
4
+ # Call a single spinner from the command-line
5
+
6
+ Whirly.start(spinner: $*[0], status: $*[2] || $*[0], use_color: false){ sleep(($*[1] || 10).to_i) }
@@ -0,0 +1,7 @@
1
+ require_relative "../lib/whirly"
2
+
3
+ Whirly.start status: "Initial status, passed when starting Whirly"
4
+ sleep 3
5
+ Whirly.status = "Status update"
6
+ sleep 3
7
+ Whirly.stop
data/lib/whirly.rb CHANGED
@@ -1,78 +1,163 @@
1
1
  require_relative "whirly/version"
2
2
  require_relative "whirly/spinners"
3
3
 
4
- require "paint"
4
+ require "unicode/display_width"
5
5
 
6
- # TODO configure extra-line below
7
- # TODO clear previous frame
6
+ begin
7
+ require "paint"
8
+ rescue LoadError
9
+ end
8
10
 
9
11
  module Whirly
12
+ @configured = false
13
+
10
14
  CLI_COMMANDS = {
11
15
  hide_cursor: "\x1b[?25l",
12
16
  show_cursor: "\x1b[?25h",
13
- }
17
+ }.freeze
18
+
19
+ DEFAULT_OPTIONS = {
20
+ ambiguous_character_width: 1,
21
+ ansi_escape_mode: "restore",
22
+ append_newline: true,
23
+ color: !!defined?(Paint),
24
+ color_change_rate: 30,
25
+ hide_cursor: true,
26
+ non_tty: false,
27
+ position: "normal",
28
+ remove_after_stop: false,
29
+ spinner: "whirly",
30
+ spinner_packs: [:whirly, :cli],
31
+ status: nil,
32
+ stream: $stdout,
33
+ }.freeze
34
+
35
+ SOFT_DEFAULT_OPTIONS = {
36
+ interval: 100,
37
+ mode: "linear",
38
+ stop: nil,
39
+ }.freeze
14
40
 
15
41
  class << self
16
42
  attr_accessor :status
17
- end
43
+ attr_reader :options
18
44
 
19
- def self.enabled?
20
- @enabled
21
- end
22
-
23
- def self.paused?
24
- @paused
45
+ def enabled?
46
+ !!(defined?(@enabled) && @enabled)
47
+ end
48
+
49
+ def configured?
50
+ !!(@configured)
51
+ end
25
52
  end
26
53
 
27
- def self.start(stream: $stdout,
28
- interval: nil,
29
- spinner: "whirly",
30
- use_color: defined?(Paint),
31
- color_change_rate: 30,
32
- status: nil,
33
- hide_cursor: true,
34
- non_tty: false)
35
- # only actviate if we are on a real terminal (or forced)
36
- return false unless stream.tty? || non_tty
37
-
38
- # ensure cursor is visible after exit
39
- at_exit{ @stream.print CLI_COMMANDS[:show_cursor] } if !defined?(@enabled) && hide_cursor
40
-
41
- # only activate once
42
- return false if @enabled
43
-
44
- # save options and preprocess
45
- @enabled = true
46
- @paused = false
47
- @stream = stream
48
- @status = status
49
- if spinner.is_a? Hash
50
- @spinner = spinner
54
+ # set spinner directly or lookup
55
+ def self.configure_spinner(spinner_option)
56
+ case spinner_option
57
+ when Hash
58
+ spinner = spinner_option.dup
59
+ when Enumerable
60
+ spinner = { "frames" => spinner_option.dup }
61
+ when Proc
62
+ spinner = { "proc" => spinner_option.dup }
51
63
  else
52
- @spinner = SPINNERS[spinner.to_s]
64
+ spinner = nil
65
+ catch(:found){
66
+ @options[:spinner_packs].each{ |spinner_pack|
67
+ spinners = Whirly::Spinners.const_get(spinner_pack.to_s.upcase)
68
+ if spinners[spinner_option]
69
+ spinner = spinners[spinner_option].dup
70
+ throw(:found)
71
+ end
72
+ }
73
+ }
53
74
  end
54
- raise(ArgumentError, "Whirly: Invalid spinner given") if !@spinner || (!@spinner["frames"] && !@spinner["proc"])
55
- @hide_cursor = hide_cursor
56
- @interval = (interval || @spinner["interval"] || 100) * 0.001
57
- @frames = @spinner["frames"] && @spinner["frames"].cycle
58
- @proc = @spinner["proc"]
59
75
 
60
- # init color
61
- if use_color
62
- if defined?(Paint)
63
- @color = "%.6x" % rand(16777216)
64
- @color_directions = (0..2).map{ |e| rand(3) - 1 }
65
- @color_change_rate = color_change_rate
76
+ # validate spinner
77
+ if !spinner || (!spinner["frames"] && !spinner["proc"])
78
+ raise(ArgumentError, "Whirly: Invalid spinner given")
79
+ end
80
+
81
+ spinner
82
+ end
83
+
84
+ # frames can be generated from enumerables or procs
85
+ def self.configure_frames(spinner)
86
+ if spinner["frames"]
87
+ case spinner["mode"]
88
+ when "swing"
89
+ frames = (spinner["frames"].to_a + spinner["frames"].to_a[1..-2].reverse).cycle
90
+ when "random"
91
+ frame_pool = spinner["frames"].to_a
92
+ frames = ->(){ frame_pool.sample }
93
+ when "reverse"
94
+ frames = spinner["frames"].to_a.reverse.cycle
66
95
  else
67
- warn "Whirly warning: Using colors requires the paint gem"
96
+ frames = spinner["frames"].cycle
97
+ end
98
+ elsif spinner["proc"]
99
+ frames = spinner["proc"].dup
100
+ else
101
+ raise(ArgumentError, "Whirly: Invalid spinner given")
102
+ end
103
+
104
+ if frames.is_a? Proc
105
+ class << frames
106
+ alias next call
68
107
  end
69
108
  end
70
109
 
110
+ frames
111
+ end
112
+
113
+ # save options and preprocess, set defaults if value is still unknown
114
+ def self.configure(**options)
115
+ if !defined?(@configured) || !@configured || !defined?(@options) || !@options
116
+ @options = DEFAULT_OPTIONS.dup
117
+ @configured = true
118
+ end
119
+
120
+ @options.merge!(options)
121
+
122
+ spinner = configure_spinner(@options[:spinner])
123
+ spinner_overwrites = {}
124
+ spinner_overwrites["mode"] = @options[:mode] if @options.key?(:mode)
125
+ @frames = configure_frames(spinner.merge(spinner_overwrites))
126
+
127
+ @interval = (@options[:interval] || spinner["interval"] || SOFT_DEFAULT_OPTIONS[:interval]) * 0.001
128
+ @stop = @options[:stop] || spinner["stop"]
129
+ @status = @options[:status]
130
+ end
131
+
132
+ def self.start(**options)
133
+ # optionally overwrite configuration on start
134
+ configure(**options)
135
+
136
+ # ensure cursor is visible after exit the program (only register for the very first time)
137
+ if (!defined?(@at_exit_handler_registered) || !@at_exit_handler_registered) && @options[:hide_cursor]
138
+ @at_exit_handler_registered = true
139
+ stream = @options[:stream]
140
+ at_exit{ stream.print CLI_COMMANDS[:show_cursor] }
141
+ end
142
+
143
+ # only enable once
144
+ return false if defined?(@enabled) && @enabled
145
+
146
+ # set status to enabled
147
+ @enabled = true
148
+
149
+ # only do something if we are on a real terminal (or forced)
150
+ return false unless @options[:stream].tty? || @options[:non_tty]
151
+
152
+ # init color
153
+ initialize_color if @options[:color]
154
+
71
155
  # hide cursor
72
- @stream.print CLI_COMMANDS[:hide_cursor] if @hide_cursor
156
+ @options[:stream].print CLI_COMMANDS[:hide_cursor] if @options[:hide_cursor]
73
157
 
74
158
  # start spinner loop
75
159
  @thread = Thread.new do
160
+ @current_frame = nil
76
161
  while true # it's just a spinner, no exact timing here
77
162
  next_color if @color
78
163
  render
@@ -80,7 +165,7 @@ module Whirly
80
165
  end
81
166
  end
82
167
 
83
- # idiomatic block syntax
168
+ # idiomatic block syntax support
84
169
  if block_given?
85
170
  yield
86
171
  Whirly.stop
@@ -89,50 +174,76 @@ module Whirly
89
174
  true
90
175
  end
91
176
 
92
- def self.stop(delete = false)
177
+ def self.stop(stop_frame = nil)
93
178
  return false unless @enabled
94
- @thread.terminate
179
+ @thread.terminate if @thread
180
+ render(stop_frame || @stop) if stop_frame || @stop
181
+ unrender if @options[:remove_after_stop]
182
+ @options[:stream].puts if @options[:append_newline]
183
+ @options[:stream].print CLI_COMMANDS[:show_cursor] if @options[:hide_cursor]
95
184
  @enabled = false
96
- @stream.print CLI_COMMANDS[:show_cursor] if @hide_cursor
97
- print "TODO" if delete
98
-
185
+
99
186
  true
100
187
  end
101
188
 
102
- def self.pause
103
- # unrender
104
- @paused = true
105
- @stream.print CLI_COMMANDS[:show_cursor] if @hide_cursor
106
- if block_given?
107
- yield
108
- continue
109
- end
189
+ def self.reset
190
+ at_exit_handler_registered = defined?(@at_exit_handler_registered) && @at_exit_handler_registered
191
+ instance_variables.each{ |iv| remove_instance_variable(iv) }
192
+ @at_exit_handler_registered = at_exit_handler_registered
193
+ @configured = false
110
194
  end
111
195
 
112
- def self.continue
113
- @stream.print CLI_COMMANDS[:hide_cursor] if @hide_cursor
114
- @paused = false
115
- end
196
+ # - - -
116
197
 
117
198
  def self.unrender
118
199
  return unless @current_frame
119
- current_frame_size = @current_frame.size
120
- @stream.print "\n\e[s#{' ' * current_frame_size}\e[u\e[1A"
200
+ case @options[:ansi_escape_mode]
201
+ when "restore"
202
+ @options[:stream].print(render_prefix + (
203
+ ' ' * (Unicode::DisplayWidth.of(@current_frame, @options[:ambiguous_character_width]) + 1)
204
+ ) + render_suffix)
205
+ when "line"
206
+ @options[:stream].print "\e[1K"
207
+ end
121
208
  end
122
209
 
123
- def self.render
124
- return if @paused
210
+ def self.render(next_frame = nil)
125
211
  unrender
126
- @current_frame = @proc ? @proc.call : @frames.next
127
- @current_frame = Paint[@current_frame, @color] if @color
212
+
213
+ @current_frame = next_frame || @frames.next
214
+ @current_frame = Paint[@current_frame, @color] if @options[:color]
128
215
  @current_frame += " #{@status}" if @status
129
- # @stream.print "\e[s#{@current_frame}\e[u"
130
- @stream.print "\n\e[s#{@current_frame}\e[u\e[1A"
216
+
217
+ @options[:stream].print(render_prefix + @current_frame.to_s + render_suffix)
218
+ end
219
+
220
+ def self.render_prefix
221
+ res = ""
222
+ res << "\n" if @options[:position] == "below"
223
+ res << "\e[s" if @options[:ansi_escape_mode] == "restore"
224
+ res << "\e[G" if @options[:ansi_escape_mode] == "line"
225
+ res
226
+ end
227
+
228
+ def self.render_suffix
229
+ res = ""
230
+ res << "\e[u" if @options[:ansi_escape_mode] == "restore"
231
+ res << "\e[1A" if @options[:position] == "below"
232
+ res
233
+ end
234
+
235
+ def self.initialize_color
236
+ if !defined?(Paint)
237
+ warn "Whirly warning: Using colors requires the paint gem"
238
+ else
239
+ @color = "%.6x" % rand(16777216)
240
+ @color_directions = (0..2).map{ |e| rand(3) - 1 }
241
+ end
131
242
  end
132
243
 
133
244
  def self.next_color
134
245
  @color = @color.scan(/../).map.with_index{ |c, i|
135
- color_change = rand(@color_change_rate) * @color_directions[i]
246
+ color_change = rand(@options[:color_change_rate]) * @color_directions[i]
136
247
  nc = c.to_i(16) + color_change
137
248
  if nc <= 0
138
249
  nc = 0