whirly 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|