dev-ui 0.1.0 → 0.1.1
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/Gemfile +2 -0
- data/Gemfile.lock +10 -2
- data/README.md +132 -2
- data/dev.yml +0 -1
- data/lib/dev/ui.rb +82 -4
- data/lib/dev/ui/ansi.rb +67 -6
- data/lib/dev/ui/color.rb +21 -0
- data/lib/dev/ui/formatter.rb +23 -2
- data/lib/dev/ui/frame.rb +128 -10
- data/lib/dev/ui/glyph.rb +43 -18
- data/lib/dev/ui/interactive_prompt.rb +81 -38
- data/lib/dev/ui/progress.rb +42 -17
- data/lib/dev/ui/prompt.rb +54 -1
- data/lib/dev/ui/spinner.rb +28 -148
- data/lib/dev/ui/spinner/async.rb +40 -0
- data/lib/dev/ui/spinner/spin_group.rb +223 -0
- data/lib/dev/ui/stdout_router.rb +3 -2
- data/lib/dev/ui/terminal.rb +3 -0
- data/lib/dev/ui/version.rb +1 -1
- metadata +5 -3
data/lib/dev/ui/progress.rb
CHANGED
@@ -3,25 +3,33 @@ require 'dev/ui'
|
|
3
3
|
module Dev
|
4
4
|
module UI
|
5
5
|
class Progress
|
6
|
-
|
7
|
-
|
6
|
+
# A Cyan filled block
|
7
|
+
FILLED_BAR = "\e[46m"
|
8
|
+
# A bright white block
|
9
|
+
UNFILLED_BAR = "\e[1;47m"
|
8
10
|
|
11
|
+
# Add a progress bar to the terminal output
|
12
|
+
#
|
13
|
+
# https://user-images.githubusercontent.com/3074765/33799794-cc4c940e-dd00-11e7-9bdc-90f77ec9167c.gif
|
14
|
+
#
|
15
|
+
# ==== Example Usage:
|
16
|
+
#
|
9
17
|
# Set the percent to X
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
18
|
+
# Dev::UI::Progress.progress do |bar|
|
19
|
+
# bar.tick(set_percent: percent)
|
20
|
+
# end
|
13
21
|
#
|
14
|
-
# Increase the percent by 1
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
22
|
+
# Increase the percent by 1 percent
|
23
|
+
# Dev::UI::Progress.progress do |bar|
|
24
|
+
# bar.tick
|
25
|
+
# end
|
18
26
|
#
|
19
27
|
# Increase the percent by X
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
def self.progress
|
24
|
-
bar = Progress.new
|
28
|
+
# Dev::UI::Progress.progress do |bar|
|
29
|
+
# bar.tick(percent: 5)
|
30
|
+
# end
|
31
|
+
def self.progress(width: Terminal.width)
|
32
|
+
bar = Progress.new(width: width)
|
25
33
|
print Dev::UI::ANSI.hide_cursor
|
26
34
|
yield(bar)
|
27
35
|
ensure
|
@@ -32,11 +40,26 @@ module Dev
|
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
43
|
+
# Initialize a progress bar. Typically used in a +Progress.progress+ block
|
44
|
+
#
|
45
|
+
# ==== Options
|
46
|
+
# One of the follow can be used, but not both together
|
47
|
+
#
|
48
|
+
# * +:width+ - The width of the terminal
|
49
|
+
#
|
35
50
|
def initialize(width: Terminal.width)
|
36
51
|
@percent_done = 0
|
37
52
|
@max_width = width
|
38
53
|
end
|
39
54
|
|
55
|
+
# Set the progress of the bar. Typically used in a +Progress.progress+ block
|
56
|
+
#
|
57
|
+
# ==== Options
|
58
|
+
# One of the follow can be used, but not both together
|
59
|
+
#
|
60
|
+
# * +:percent+ - Increment progress by a specific percent amount
|
61
|
+
# * +:set_percent+ - Set progress to a specific percent
|
62
|
+
#
|
40
63
|
def tick(percent: 0.01, set_percent: nil)
|
41
64
|
raise ArgumentError, 'percent and set_percent cannot both be specified' if percent != 0.01 && set_percent
|
42
65
|
@percent_done += percent
|
@@ -48,6 +71,8 @@ module Dev
|
|
48
71
|
print Dev::UI::ANSI.end_of_line + "\n"
|
49
72
|
end
|
50
73
|
|
74
|
+
# Format the progress bar to be printed to terminal
|
75
|
+
#
|
51
76
|
def to_s
|
52
77
|
suffix = " #{(@percent_done * 100).round(2)}%"
|
53
78
|
workable_width = @max_width - Frame.prefix_width - suffix.size
|
@@ -55,9 +80,9 @@ module Dev
|
|
55
80
|
unfilled = workable_width - filled
|
56
81
|
|
57
82
|
Dev::UI.resolve_text [
|
58
|
-
|
59
|
-
|
60
|
-
suffix
|
83
|
+
FILLED_BAR + ' ' * filled,
|
84
|
+
UNFILLED_BAR + ' ' * unfilled,
|
85
|
+
Dev::UI::Color::RESET.code + suffix
|
61
86
|
].join
|
62
87
|
end
|
63
88
|
end
|
data/lib/dev/ui/prompt.rb
CHANGED
@@ -5,6 +5,50 @@ module Dev
|
|
5
5
|
module UI
|
6
6
|
module Prompt
|
7
7
|
class << self
|
8
|
+
# Ask a user a question with either free form answer or a set of answers
|
9
|
+
# Do not use this method for yes/no questions. Use +confirm+
|
10
|
+
# Can use arrows, y/n, numbers, and vim bindings to control
|
11
|
+
#
|
12
|
+
# * Handles free form answers (options are nil)
|
13
|
+
# * Handles default answers for free form text
|
14
|
+
# * Handles file auto completion for file input
|
15
|
+
# * Handles interactively choosing answers using +InteractivePrompt+
|
16
|
+
#
|
17
|
+
# https://user-images.githubusercontent.com/3074765/33799822-47f23302-dd01-11e7-82f3-9072a5a5f611.png
|
18
|
+
#
|
19
|
+
# ==== Attributes
|
20
|
+
#
|
21
|
+
# * +question+ - (required) The question to ask the user
|
22
|
+
#
|
23
|
+
# ==== Options
|
24
|
+
#
|
25
|
+
# * +:options+ - Options to ask the user. Will use +InteractivePrompt+ to do so
|
26
|
+
# * +:default+ - The default answer to the question (e.g. they just press enter and don't input anything)
|
27
|
+
# * +:is_file+ - Tells the input to use file auto-completion (tab completion)
|
28
|
+
# * +:allow_empty+ - Allows the answer to be empty
|
29
|
+
#
|
30
|
+
# Note:
|
31
|
+
# * +:options+ conflicts with +:default+ and +:is_file+, you cannot set options with either of these keywords
|
32
|
+
# * +:default+ conflicts with +:allow_empty:, you cannot set these together
|
33
|
+
#
|
34
|
+
# ==== Example Usage:
|
35
|
+
#
|
36
|
+
# Free form question
|
37
|
+
# Dev::UI::Prompt.ask('What color is the sky?')
|
38
|
+
#
|
39
|
+
# Free form question with a file answer
|
40
|
+
# Dev::UI::Prompt.ask('Where is your Gemfile located?', is_file: true)
|
41
|
+
#
|
42
|
+
# Free form question with a default answer
|
43
|
+
# Dev::UI::Prompt.ask('What color is the sky?', default: 'blue')
|
44
|
+
#
|
45
|
+
# Free form question when the answer can be empty
|
46
|
+
# Dev::UI::Prompt.ask('What is your opinion on this question?', allow_empty: true)
|
47
|
+
#
|
48
|
+
# Question with answers
|
49
|
+
# Dev::UI::Prompt.ask('What kind of project is this?', options: %w(rails go ruby python))
|
50
|
+
#
|
51
|
+
#
|
8
52
|
def ask(question, options: nil, default: nil, is_file: nil, allow_empty: true)
|
9
53
|
if (default && !allow_empty) || (options && (default || is_file))
|
10
54
|
raise(ArgumentError, 'conflicting arguments')
|
@@ -34,6 +78,14 @@ module Dev
|
|
34
78
|
end
|
35
79
|
end
|
36
80
|
|
81
|
+
# Asks the user a yes/no question.
|
82
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
83
|
+
#
|
84
|
+
# ==== Example Usage:
|
85
|
+
#
|
86
|
+
# Free form question
|
87
|
+
# Dev::UI::Prompt.confirm('Is the sky blue?')
|
88
|
+
#
|
37
89
|
def confirm(question)
|
38
90
|
puts_question("#{question} {{yellow:(choose with ↑ ↓ ⏎)}}")
|
39
91
|
InteractivePrompt.call(%w(yes no)) == 'yes'
|
@@ -76,7 +128,8 @@ module Dev
|
|
76
128
|
prompt = prefix + Dev::UI.fmt('{{blue:> }}{{yellow:')
|
77
129
|
|
78
130
|
begin
|
79
|
-
Readline.readline(prompt, true)
|
131
|
+
line = Readline.readline(prompt, true)
|
132
|
+
line.to_s.chomp
|
80
133
|
rescue Interrupt
|
81
134
|
Dev::UI.raw { STDERR.puts('^C' + Dev::UI::Color::RESET.code) }
|
82
135
|
raise
|
data/lib/dev/ui/spinner.rb
CHANGED
@@ -3,7 +3,11 @@ require 'dev/ui'
|
|
3
3
|
module Dev
|
4
4
|
module UI
|
5
5
|
module Spinner
|
6
|
+
autoload :Async, 'dev/ui/spinner/async'
|
7
|
+
autoload :SpinGroup, 'dev/ui/spinner/spin_group'
|
8
|
+
|
6
9
|
PERIOD = 0.1 # seconds
|
10
|
+
TASK_FAILED = :task_failed
|
7
11
|
|
8
12
|
begin
|
9
13
|
runes = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
@@ -12,157 +16,33 @@ module Dev
|
|
12
16
|
GLYPHS = colors.zip(runes).map(&:join)
|
13
17
|
end
|
14
18
|
|
15
|
-
|
16
|
-
|
19
|
+
# Adds a single spinner
|
20
|
+
# Uses an interactive session to allow the user to pick an answer
|
21
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
22
|
+
#
|
23
|
+
# https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif
|
24
|
+
#
|
25
|
+
# ==== Attributes
|
26
|
+
#
|
27
|
+
# * +title+ - Title of the spinner to use
|
28
|
+
#
|
29
|
+
# ==== Options
|
30
|
+
#
|
31
|
+
# * +:auto_debrief+ - Automatically debrief exceptions? Default to true
|
32
|
+
#
|
33
|
+
# ==== Block
|
34
|
+
#
|
35
|
+
# * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes
|
36
|
+
#
|
37
|
+
# ==== Example Usage:
|
38
|
+
#
|
39
|
+
# Dev::UI::Spinner.spin('Title') { sleep 1.0 }
|
40
|
+
#
|
41
|
+
def self.spin(title, auto_debrief: true, &block)
|
42
|
+
sg = SpinGroup.new(auto_debrief: auto_debrief)
|
17
43
|
sg.add(title, &block)
|
18
44
|
sg.wait
|
19
45
|
end
|
20
|
-
|
21
|
-
class SpinGroup
|
22
|
-
def initialize
|
23
|
-
@m = Mutex.new
|
24
|
-
@consumed_lines = 0
|
25
|
-
@tasks = []
|
26
|
-
end
|
27
|
-
|
28
|
-
class Task
|
29
|
-
attr_reader :title, :exception, :success, :stdout, :stderr
|
30
|
-
|
31
|
-
def initialize(title, &block)
|
32
|
-
@title = title
|
33
|
-
@thread = Thread.new do
|
34
|
-
cap = Dev::UI::StdoutRouter::Capture.new(with_frame_inset: false, &block)
|
35
|
-
begin
|
36
|
-
cap.run
|
37
|
-
ensure
|
38
|
-
@stdout = cap.stdout
|
39
|
-
@stderr = cap.stderr
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
@done = false
|
44
|
-
@exception = nil
|
45
|
-
@success = false
|
46
|
-
end
|
47
|
-
|
48
|
-
def check
|
49
|
-
return true if @done
|
50
|
-
return false if @thread.alive?
|
51
|
-
|
52
|
-
@done = true
|
53
|
-
begin
|
54
|
-
status = @thread.join.status
|
55
|
-
@success = (status == false)
|
56
|
-
rescue => exc
|
57
|
-
@exception = exc
|
58
|
-
@success = false
|
59
|
-
end
|
60
|
-
|
61
|
-
@done
|
62
|
-
end
|
63
|
-
|
64
|
-
def render(index, force = true)
|
65
|
-
return full_render(index) if force
|
66
|
-
partial_render(index)
|
67
|
-
end
|
68
|
-
|
69
|
-
private
|
70
|
-
|
71
|
-
def full_render(index)
|
72
|
-
inset + glyph(index) + Dev::UI::Color::RESET.code + ' ' + Dev::UI.resolve_text(title)
|
73
|
-
end
|
74
|
-
|
75
|
-
def partial_render(index)
|
76
|
-
Dev::UI::ANSI.cursor_forward(inset_width) + glyph(index) + Dev::UI::Color::RESET.code
|
77
|
-
end
|
78
|
-
|
79
|
-
def glyph(index)
|
80
|
-
if @done
|
81
|
-
@success ? Dev::UI::Glyph::CHECK.to_s : Dev::UI::Glyph::X.to_s
|
82
|
-
else
|
83
|
-
GLYPHS[index]
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def inset
|
88
|
-
@inset ||= Dev::UI::Frame.prefix
|
89
|
-
end
|
90
|
-
|
91
|
-
def inset_width
|
92
|
-
@inset_width ||= Dev::UI::ANSI.printing_width(inset)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
def add(title, &block)
|
97
|
-
@m.synchronize do
|
98
|
-
@tasks << Task.new(title, &block)
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def wait
|
103
|
-
idx = 0
|
104
|
-
|
105
|
-
loop do
|
106
|
-
all_done = true
|
107
|
-
|
108
|
-
@m.synchronize do
|
109
|
-
Dev::UI.raw do
|
110
|
-
@tasks.each.with_index do |task, int_index|
|
111
|
-
nat_index = int_index + 1
|
112
|
-
task_done = task.check
|
113
|
-
all_done = false unless task_done
|
114
|
-
|
115
|
-
if nat_index > @consumed_lines
|
116
|
-
print(task.render(idx, true) + "\n")
|
117
|
-
@consumed_lines += 1
|
118
|
-
else
|
119
|
-
offset = @consumed_lines - int_index
|
120
|
-
move_to = Dev::UI::ANSI.cursor_up(offset) + "\r"
|
121
|
-
move_from = "\r" + Dev::UI::ANSI.cursor_down(offset)
|
122
|
-
|
123
|
-
print(move_to + task.render(idx, idx.zero?) + move_from)
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
break if all_done
|
130
|
-
|
131
|
-
idx = (idx + 1) % GLYPHS.size
|
132
|
-
sleep(PERIOD)
|
133
|
-
end
|
134
|
-
|
135
|
-
debrief
|
136
|
-
end
|
137
|
-
|
138
|
-
def debrief
|
139
|
-
@m.synchronize do
|
140
|
-
@tasks.each do |task|
|
141
|
-
next if task.success
|
142
|
-
|
143
|
-
e = task.exception
|
144
|
-
out = task.stdout
|
145
|
-
err = task.stderr
|
146
|
-
|
147
|
-
Dev::UI::Frame.open('Task Failed: ' + task.title, color: :red) do
|
148
|
-
if e
|
149
|
-
puts"#{e.class}: #{e.message}"
|
150
|
-
puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
|
151
|
-
end
|
152
|
-
|
153
|
-
Dev::UI::Frame.divider('STDOUT')
|
154
|
-
out = "(empty)" if out.nil? || out.strip.empty?
|
155
|
-
puts out
|
156
|
-
|
157
|
-
Dev::UI::Frame.divider('STDERR')
|
158
|
-
err = "(empty)" if err.nil? || err.strip.empty?
|
159
|
-
puts err
|
160
|
-
end
|
161
|
-
end
|
162
|
-
@tasks.all?(&:success)
|
163
|
-
end
|
164
|
-
end
|
165
|
-
end
|
166
46
|
end
|
167
47
|
end
|
168
48
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Dev
|
2
|
+
module UI
|
3
|
+
module Spinner
|
4
|
+
class Async
|
5
|
+
# Convenience method for +initialize+
|
6
|
+
#
|
7
|
+
def self.start(title)
|
8
|
+
new(title)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Initializes a new asynchronous spinner with no specific end.
|
12
|
+
# Must call +.stop+ to end the spinner
|
13
|
+
#
|
14
|
+
# ==== Attributes
|
15
|
+
#
|
16
|
+
# * +title+ - Title of the spinner to use
|
17
|
+
#
|
18
|
+
# ==== Example Usage:
|
19
|
+
#
|
20
|
+
# Dev::UI::Spinner::Async.new('Title')
|
21
|
+
#
|
22
|
+
def initialize(title)
|
23
|
+
require 'thread'
|
24
|
+
sg = Dev::UI::Spinner::SpinGroup.new
|
25
|
+
@m = Mutex.new
|
26
|
+
@cv = ConditionVariable.new
|
27
|
+
sg.add(title) { @m.synchronize { @cv.wait(@m) } }
|
28
|
+
@t = Thread.new { sg.wait }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Stops an asynchronous spinner
|
32
|
+
#
|
33
|
+
def stop
|
34
|
+
@m.synchronize { @cv.signal }
|
35
|
+
@t.value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
module Dev
|
2
|
+
module UI
|
3
|
+
module Spinner
|
4
|
+
class SpinGroup
|
5
|
+
# Initializes a new spin group
|
6
|
+
# This lets you add +Task+ objects to the group to multi-thread work
|
7
|
+
#
|
8
|
+
# ==== Options
|
9
|
+
#
|
10
|
+
# * +:auto_debrief+ - Automatically debrief exceptions? Default to true
|
11
|
+
#
|
12
|
+
# ==== Example Usage
|
13
|
+
#
|
14
|
+
# spin_group = Dev::UI::SpinGroup.new
|
15
|
+
# spin_group.add('Title') { |spinner| sleep 3.0 }
|
16
|
+
# spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
|
17
|
+
# spin_group.wait
|
18
|
+
#
|
19
|
+
# Output:
|
20
|
+
#
|
21
|
+
# https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif
|
22
|
+
#
|
23
|
+
def initialize(auto_debrief: true)
|
24
|
+
@m = Mutex.new
|
25
|
+
@consumed_lines = 0
|
26
|
+
@tasks = []
|
27
|
+
@auto_debrief = auto_debrief
|
28
|
+
end
|
29
|
+
|
30
|
+
class Task
|
31
|
+
attr_reader :title, :exception, :success, :stdout, :stderr
|
32
|
+
|
33
|
+
# Initializes a new Task
|
34
|
+
# This is managed entirely internally by +SpinGroup+
|
35
|
+
#
|
36
|
+
# ==== Attributes
|
37
|
+
#
|
38
|
+
# * +title+ - Title of the task
|
39
|
+
# * +block+ - Block for the task, will be provided with an instance of the spinner
|
40
|
+
#
|
41
|
+
def initialize(title, &block)
|
42
|
+
@title = title
|
43
|
+
@thread = Thread.new do
|
44
|
+
cap = Dev::UI::StdoutRouter::Capture.new(self, with_frame_inset: false, &block)
|
45
|
+
begin
|
46
|
+
cap.run
|
47
|
+
ensure
|
48
|
+
@stdout = cap.stdout
|
49
|
+
@stderr = cap.stderr
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
@force_full_render = false
|
54
|
+
@done = false
|
55
|
+
@exception = nil
|
56
|
+
@success = false
|
57
|
+
end
|
58
|
+
|
59
|
+
# Checks if a task is finished
|
60
|
+
#
|
61
|
+
def check
|
62
|
+
return true if @done
|
63
|
+
return false if @thread.alive?
|
64
|
+
|
65
|
+
@done = true
|
66
|
+
begin
|
67
|
+
status = @thread.join.status
|
68
|
+
@success = (status == false)
|
69
|
+
@success = false if @thread.value == TASK_FAILED
|
70
|
+
rescue => exc
|
71
|
+
@exception = exc
|
72
|
+
@success = false
|
73
|
+
end
|
74
|
+
|
75
|
+
@done
|
76
|
+
end
|
77
|
+
|
78
|
+
# Re-renders the task if required
|
79
|
+
#
|
80
|
+
# ==== Attributes
|
81
|
+
#
|
82
|
+
# * +index+ - index of the task
|
83
|
+
# * +force+ - force rerender of the task
|
84
|
+
#
|
85
|
+
def render(index, force = true)
|
86
|
+
return full_render(index) if force || @force_full_render
|
87
|
+
partial_render(index)
|
88
|
+
ensure
|
89
|
+
@force_full_render = false
|
90
|
+
end
|
91
|
+
|
92
|
+
# Update the spinner title
|
93
|
+
#
|
94
|
+
# ==== Attributes
|
95
|
+
#
|
96
|
+
# * +title+ - title to change the spinner to
|
97
|
+
#
|
98
|
+
def update_title(new_title)
|
99
|
+
@title = new_title
|
100
|
+
@force_full_render = true
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def full_render(index)
|
106
|
+
inset + glyph(index) + Dev::UI::Color::RESET.code + ' ' + Dev::UI.resolve_text(title) + "\e[K"
|
107
|
+
end
|
108
|
+
|
109
|
+
def partial_render(index)
|
110
|
+
Dev::UI::ANSI.cursor_forward(inset_width) + glyph(index) + Dev::UI::Color::RESET.code
|
111
|
+
end
|
112
|
+
|
113
|
+
def glyph(index)
|
114
|
+
if @done
|
115
|
+
@success ? Dev::UI::Glyph::CHECK.to_s : Dev::UI::Glyph::X.to_s
|
116
|
+
else
|
117
|
+
GLYPHS[index]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def inset
|
122
|
+
@inset ||= Dev::UI::Frame.prefix
|
123
|
+
end
|
124
|
+
|
125
|
+
def inset_width
|
126
|
+
@inset_width ||= Dev::UI::ANSI.printing_width(inset)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Add a new task
|
131
|
+
#
|
132
|
+
# ==== Attributes
|
133
|
+
#
|
134
|
+
# * +title+ - Title of the task
|
135
|
+
# * +block+ - Block for the task, will be provided with an instance of the spinner
|
136
|
+
#
|
137
|
+
# ==== Example Usage:
|
138
|
+
# spin_group = Dev::UI::SpinGroup.new
|
139
|
+
# spin_group.add('Title') { |spinner| sleep 1.0 }
|
140
|
+
# spin_group.wait
|
141
|
+
#
|
142
|
+
def add(title, &block)
|
143
|
+
@m.synchronize do
|
144
|
+
@tasks << Task.new(title, &block)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Tells the group you're done adding tasks and to wait for all of them to finish
|
149
|
+
#
|
150
|
+
# ==== Example Usage:
|
151
|
+
# spin_group = Dev::UI::SpinGroup.new
|
152
|
+
# spin_group.add('Title') { |spinner| sleep 1.0 }
|
153
|
+
# spin_group.wait
|
154
|
+
#
|
155
|
+
def wait
|
156
|
+
idx = 0
|
157
|
+
|
158
|
+
loop do
|
159
|
+
all_done = true
|
160
|
+
|
161
|
+
@m.synchronize do
|
162
|
+
Dev::UI.raw do
|
163
|
+
@tasks.each.with_index do |task, int_index|
|
164
|
+
nat_index = int_index + 1
|
165
|
+
task_done = task.check
|
166
|
+
all_done = false unless task_done
|
167
|
+
|
168
|
+
if nat_index > @consumed_lines
|
169
|
+
print(task.render(idx, true) + "\n")
|
170
|
+
@consumed_lines += 1
|
171
|
+
else
|
172
|
+
offset = @consumed_lines - int_index
|
173
|
+
move_to = Dev::UI::ANSI.cursor_up(offset) + "\r"
|
174
|
+
move_from = "\r" + Dev::UI::ANSI.cursor_down(offset)
|
175
|
+
|
176
|
+
print(move_to + task.render(idx, idx.zero?) + move_from)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
break if all_done
|
183
|
+
|
184
|
+
idx = (idx + 1) % GLYPHS.size
|
185
|
+
sleep(PERIOD)
|
186
|
+
end
|
187
|
+
|
188
|
+
debrief if @auto_debrief
|
189
|
+
end
|
190
|
+
|
191
|
+
# Debriefs failed tasks is +auto_debrief+ is true
|
192
|
+
#
|
193
|
+
def debrief
|
194
|
+
@m.synchronize do
|
195
|
+
@tasks.each do |task|
|
196
|
+
next if task.success
|
197
|
+
|
198
|
+
e = task.exception
|
199
|
+
out = task.stdout
|
200
|
+
err = task.stderr
|
201
|
+
|
202
|
+
Dev::UI::Frame.open('Task Failed: ' + task.title, color: :red) do
|
203
|
+
if e
|
204
|
+
puts"#{e.class}: #{e.message}"
|
205
|
+
puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
|
206
|
+
end
|
207
|
+
|
208
|
+
Dev::UI::Frame.divider('STDOUT')
|
209
|
+
out = "(empty)" if out.nil? || out.strip.empty?
|
210
|
+
puts out
|
211
|
+
|
212
|
+
Dev::UI::Frame.divider('STDERR')
|
213
|
+
err = "(empty)" if err.nil? || err.strip.empty?
|
214
|
+
puts err
|
215
|
+
end
|
216
|
+
end
|
217
|
+
@tasks.all?(&:success)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|