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.
- checksums.yaml +4 -4
- data/.travis.yml +5 -0
- data/CHANGELOG.md +18 -1
- data/Gemfile +1 -0
- data/README.md +201 -19
- data/Rakefile +23 -2
- data/data/{spinners.json → cli-spinners.json} +92 -0
- data/data/whirly-static-spinners.json +86 -0
- data/examples/all_spinners.rb +21 -0
- data/examples/asciinema_bundled_spinners.rb +12 -0
- data/{euruko.rb → examples/euruko.rb} +4 -42
- data/examples/multi_lines.rb +20 -0
- data/examples/single.rb +6 -0
- data/examples/status.rb +7 -0
- data/lib/whirly.rb +187 -76
- data/lib/whirly/spinners.rb +5 -4
- data/lib/whirly/spinners/cli.rb +7 -0
- data/lib/whirly/spinners/whirly.rb +20 -0
- data/lib/whirly/version.rb +1 -1
- data/spec/whirly_spec.rb +231 -10
- data/whirly.gemspec +5 -2
- metadata +29 -9
- data/lib/whirly/.spinners.rb.swp +0 -0
- data/spec/.whirly_spec.rb.swp +0 -0
@@ -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:
|
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"
|
data/examples/single.rb
ADDED
data/examples/status.rb
ADDED
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 "
|
4
|
+
require "unicode/display_width"
|
5
5
|
|
6
|
-
|
7
|
-
|
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
|
-
|
43
|
+
attr_reader :options
|
18
44
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
45
|
+
def enabled?
|
46
|
+
!!(defined?(@enabled) && @enabled)
|
47
|
+
end
|
48
|
+
|
49
|
+
def configured?
|
50
|
+
!!(@configured)
|
51
|
+
end
|
25
52
|
end
|
26
53
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
#
|
61
|
-
if
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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(
|
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
|
-
|
97
|
-
print "TODO" if delete
|
98
|
-
|
185
|
+
|
99
186
|
true
|
100
187
|
end
|
101
188
|
|
102
|
-
def self.
|
103
|
-
|
104
|
-
|
105
|
-
@
|
106
|
-
|
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
|
-
|
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
|
-
|
120
|
-
|
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
|
-
|
127
|
-
@current_frame =
|
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
|
-
|
130
|
-
@stream.print
|
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
|