dev-ui 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +23 -0
- data/.travis.yml +5 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +20 -0
- data/bin/console +14 -0
- data/bin/testunit +20 -0
- data/dev-ui.gemspec +27 -0
- data/dev.yml +8 -0
- data/lib/dev/ui.rb +85 -0
- data/lib/dev/ui/ansi.rb +74 -0
- data/lib/dev/ui/box.rb +15 -0
- data/lib/dev/ui/color.rb +58 -0
- data/lib/dev/ui/formatter.rb +155 -0
- data/lib/dev/ui/frame.rb +176 -0
- data/lib/dev/ui/glyph.rb +49 -0
- data/lib/dev/ui/interactive_prompt.rb +114 -0
- data/lib/dev/ui/progress.rb +65 -0
- data/lib/dev/ui/prompt.rb +88 -0
- data/lib/dev/ui/spinner.rb +168 -0
- data/lib/dev/ui/stdout_router.rb +189 -0
- data/lib/dev/ui/terminal.rb +18 -0
- data/lib/dev/ui/version.rb +5 -0
- metadata +111 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'dev/ui'
|
2
|
+
|
3
|
+
module Dev
|
4
|
+
module UI
|
5
|
+
class Progress
|
6
|
+
FILLED_BAR = Dev::UI::Glyph.new("◾", 0x2588, Color::CYAN)
|
7
|
+
UNFILLED_BAR = Dev::UI::Glyph.new("◽", 0x2588, Color::WHITE)
|
8
|
+
|
9
|
+
# Set the percent to X
|
10
|
+
# Dev::UI::Progress.progress do |bar|
|
11
|
+
# bar.tick(set_percent: percent)
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# Increase the percent by 1
|
15
|
+
# Dev::UI::Progress.progress do |bar|
|
16
|
+
# bar.tick
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Increase the percent by X
|
20
|
+
# Dev::UI::Progress.progress do |bar|
|
21
|
+
# bar.tick(percent: 5)
|
22
|
+
# end
|
23
|
+
def self.progress
|
24
|
+
bar = Progress.new
|
25
|
+
print Dev::UI::ANSI.hide_cursor
|
26
|
+
yield(bar)
|
27
|
+
ensure
|
28
|
+
puts bar.to_s
|
29
|
+
Dev::UI.raw do
|
30
|
+
print(ANSI.show_cursor)
|
31
|
+
puts(ANSI.previous_line + ANSI.end_of_line)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(width: Terminal.width)
|
36
|
+
@percent_done = 0
|
37
|
+
@max_width = width
|
38
|
+
end
|
39
|
+
|
40
|
+
def tick(percent: 0.01, set_percent: nil)
|
41
|
+
raise ArgumentError, 'percent and set_percent cannot both be specified' if percent != 0.01 && set_percent
|
42
|
+
@percent_done += percent
|
43
|
+
@percent_done = set_percent if set_percent
|
44
|
+
@percent_done = [@percent_done, 1.0].min # Make sure we can't go above 1.0
|
45
|
+
|
46
|
+
print to_s
|
47
|
+
print Dev::UI::ANSI.previous_line
|
48
|
+
print Dev::UI::ANSI.end_of_line + "\n"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
suffix = " #{(@percent_done * 100).round(2)}%"
|
53
|
+
workable_width = @max_width - Frame.prefix_width - suffix.size
|
54
|
+
filled = (@percent_done * workable_width.to_f).ceil
|
55
|
+
unfilled = workable_width - filled
|
56
|
+
|
57
|
+
Dev::UI.resolve_text [
|
58
|
+
(FILLED_BAR.to_s * filled),
|
59
|
+
(UNFILLED_BAR.to_s * unfilled),
|
60
|
+
suffix
|
61
|
+
].join
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'dev/ui'
|
2
|
+
require 'readline'
|
3
|
+
|
4
|
+
module Dev
|
5
|
+
module UI
|
6
|
+
module Prompt
|
7
|
+
class << self
|
8
|
+
def ask(question, options: nil, default: nil, is_file: nil, allow_empty: true)
|
9
|
+
if (default && !allow_empty) || (options && (default || is_file))
|
10
|
+
raise(ArgumentError, 'conflicting arguments')
|
11
|
+
end
|
12
|
+
|
13
|
+
if default
|
14
|
+
puts_question("#{question} (empty = #{default})")
|
15
|
+
elsif options
|
16
|
+
puts_question("#{question} {{yellow:(choose with ↑ ↓ ⏎)}}")
|
17
|
+
else
|
18
|
+
puts_question(question)
|
19
|
+
end
|
20
|
+
|
21
|
+
return InteractivePrompt.call(options) if options
|
22
|
+
|
23
|
+
loop do
|
24
|
+
line = readline(is_file: is_file)
|
25
|
+
|
26
|
+
if line.empty? && default
|
27
|
+
write_default_over_empty_input(default)
|
28
|
+
return default
|
29
|
+
end
|
30
|
+
|
31
|
+
if !line.empty? || allow_empty
|
32
|
+
return line
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def confirm(question)
|
38
|
+
puts_question("#{question} {{yellow:(choose with ↑ ↓ ⏎)}}")
|
39
|
+
InteractivePrompt.call(%w(yes no)) == 'yes'
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def write_default_over_empty_input(default)
|
45
|
+
Dev::UI.raw do
|
46
|
+
STDERR.puts(
|
47
|
+
Dev::UI::ANSI.cursor_up(1) +
|
48
|
+
"\r" +
|
49
|
+
Dev::UI::ANSI.cursor_forward(4) + # TODO: width
|
50
|
+
default +
|
51
|
+
Dev::UI::Color::RESET.code
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def puts_question(str)
|
57
|
+
Dev::UI.with_frame_color(:blue) do
|
58
|
+
STDOUT.puts(Dev::UI.fmt('{{?}} ' + str))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def readline(is_file: false)
|
63
|
+
if is_file
|
64
|
+
Readline.completion_proc = Readline::FILENAME_COMPLETION_PROC
|
65
|
+
Readline.completion_append_character = ""
|
66
|
+
else
|
67
|
+
Readline.completion_proc = proc { |*| nil }
|
68
|
+
Readline.completion_append_character = " "
|
69
|
+
end
|
70
|
+
|
71
|
+
# because Readline is a C library, Dev::UI's hooks into $stdout don't
|
72
|
+
# work. We could work around this by having Dev::UI use a pipe and a
|
73
|
+
# thread to manage output, but the current strategy feels like a
|
74
|
+
# better tradeoff.
|
75
|
+
prefix = Dev::UI.with_frame_color(:blue) { Dev::UI::Frame.prefix }
|
76
|
+
prompt = prefix + Dev::UI.fmt('{{blue:> }}{{yellow:')
|
77
|
+
|
78
|
+
begin
|
79
|
+
Readline.readline(prompt, true).chomp
|
80
|
+
rescue Interrupt
|
81
|
+
Dev::UI.raw { STDERR.puts('^C' + Dev::UI::Color::RESET.code) }
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'dev/ui'
|
2
|
+
|
3
|
+
module Dev
|
4
|
+
module UI
|
5
|
+
module Spinner
|
6
|
+
PERIOD = 0.1 # seconds
|
7
|
+
|
8
|
+
begin
|
9
|
+
runes = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
10
|
+
colors = [Dev::UI::Color::CYAN.code] * 5 + [Dev::UI::Color::MAGENTA.code] * 5
|
11
|
+
raise unless runes.size == colors.size
|
12
|
+
GLYPHS = colors.zip(runes).map(&:join)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.spin(title, &block)
|
16
|
+
sg = SpinGroup.new
|
17
|
+
sg.add(title, &block)
|
18
|
+
sg.wait
|
19
|
+
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.strip.empty?
|
155
|
+
puts out
|
156
|
+
|
157
|
+
Dev::UI::Frame.divider('STDERR')
|
158
|
+
err = "(empty)" if err.strip.empty?
|
159
|
+
puts err
|
160
|
+
end
|
161
|
+
end
|
162
|
+
@tasks.all?(&:success)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'dev/ui'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module Dev
|
5
|
+
module UI
|
6
|
+
module StdoutRouter
|
7
|
+
class << self
|
8
|
+
attr_accessor :duplicate_output_to
|
9
|
+
end
|
10
|
+
|
11
|
+
class Writer
|
12
|
+
def initialize(stream, name)
|
13
|
+
@stream = stream
|
14
|
+
@name = name
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(*args)
|
18
|
+
if auto_frame_inset?
|
19
|
+
str = args[0].dup # unfreeze
|
20
|
+
str = str.force_encoding(Encoding::UTF_8)
|
21
|
+
str = apply_line_prefix(str, Dev::UI::Frame.prefix)
|
22
|
+
args[0] = str
|
23
|
+
else
|
24
|
+
@pending_newline = false
|
25
|
+
end
|
26
|
+
|
27
|
+
hook = Thread.current[:devui_output_hook]
|
28
|
+
# hook return of false suppresses output.
|
29
|
+
if !hook || hook.call(args.first, @name) != false
|
30
|
+
args.first
|
31
|
+
@stream.write_without_dev_ui(*args)
|
32
|
+
if dup = StdoutRouter.duplicate_output_to
|
33
|
+
dup.write(*args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def auto_frame_inset?
|
41
|
+
!Thread.current[:no_devui_frame_inset]
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply_line_prefix(str, prefix)
|
45
|
+
return '' if str.empty?
|
46
|
+
prefixed = String.new
|
47
|
+
str.force_encoding(Encoding::UTF_8).lines.each do |line|
|
48
|
+
if @pending_newline
|
49
|
+
prefixed << line
|
50
|
+
@pending_newline = false
|
51
|
+
else
|
52
|
+
prefixed << prefix << line
|
53
|
+
end
|
54
|
+
end
|
55
|
+
@pending_newline = !str.end_with?("\n")
|
56
|
+
prefixed
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Capture
|
61
|
+
@m = Mutex.new
|
62
|
+
@active_captures = 0
|
63
|
+
@saved_stdin = nil
|
64
|
+
|
65
|
+
def self.with_stdin_masked
|
66
|
+
@m.synchronize do
|
67
|
+
if @active_captures.zero?
|
68
|
+
@saved_stdin = $stdin
|
69
|
+
$stdin, w = IO.pipe
|
70
|
+
$stdin.close
|
71
|
+
w.close
|
72
|
+
end
|
73
|
+
@active_captures += 1
|
74
|
+
end
|
75
|
+
|
76
|
+
yield
|
77
|
+
ensure
|
78
|
+
@m.synchronize do
|
79
|
+
@active_captures -= 1
|
80
|
+
if @active_captures.zero?
|
81
|
+
$stdin = @saved_stdin
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def initialize(with_frame_inset: true, &block)
|
87
|
+
@with_frame_inset = with_frame_inset
|
88
|
+
@block = block
|
89
|
+
end
|
90
|
+
|
91
|
+
attr_reader :stdout, :stderr
|
92
|
+
|
93
|
+
def run
|
94
|
+
require 'stringio'
|
95
|
+
|
96
|
+
StdoutRouter.assert_enabled!
|
97
|
+
|
98
|
+
out = StringIO.new
|
99
|
+
err = StringIO.new
|
100
|
+
|
101
|
+
prev_frame_inset = Thread.current[:no_devui_frame_inset]
|
102
|
+
prev_hook = Thread.current[:devui_output_hook]
|
103
|
+
|
104
|
+
self.class.with_stdin_masked do
|
105
|
+
Thread.current[:no_devui_frame_inset] = !@with_frame_inset
|
106
|
+
Thread.current[:devui_output_hook] = ->(data, stream) do
|
107
|
+
case stream
|
108
|
+
when :stdout then out.write(data)
|
109
|
+
when :stderr then err.write(data)
|
110
|
+
else raise
|
111
|
+
end
|
112
|
+
false # suppress writing to terminal
|
113
|
+
end
|
114
|
+
|
115
|
+
begin
|
116
|
+
@block.call
|
117
|
+
ensure
|
118
|
+
@stdout = out.string
|
119
|
+
@stderr = err.string
|
120
|
+
end
|
121
|
+
end
|
122
|
+
ensure
|
123
|
+
Thread.current[:devui_output_hook] = prev_hook
|
124
|
+
Thread.current[:no_devui_frame_inset] = prev_frame_inset
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
class << self
|
129
|
+
WRITE_WITHOUT_DEV_UI = :write_without_dev_ui
|
130
|
+
|
131
|
+
NotEnabled = Class.new(StandardError)
|
132
|
+
|
133
|
+
def assert_enabled!
|
134
|
+
raise NotEnabled unless enabled?
|
135
|
+
end
|
136
|
+
|
137
|
+
def with_enabled
|
138
|
+
enable
|
139
|
+
yield
|
140
|
+
ensure
|
141
|
+
disable
|
142
|
+
end
|
143
|
+
|
144
|
+
# TODO: remove this
|
145
|
+
def ensure_activated
|
146
|
+
enable unless enabled?
|
147
|
+
end
|
148
|
+
|
149
|
+
def enable
|
150
|
+
return false if enabled?($stdout) || enabled?($stderr)
|
151
|
+
activate($stdout, :stdout)
|
152
|
+
activate($stderr, :stderr)
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
def enabled?(stream = $stdout)
|
157
|
+
stream.respond_to?(WRITE_WITHOUT_DEV_UI)
|
158
|
+
end
|
159
|
+
|
160
|
+
def disable
|
161
|
+
return false unless enabled?($stdout) && enabled?($stderr)
|
162
|
+
deactivate($stdout)
|
163
|
+
deactivate($stderr)
|
164
|
+
true
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def deactivate(stream)
|
170
|
+
sc = stream.singleton_class
|
171
|
+
sc.send(:remove_method, :write)
|
172
|
+
sc.send(:alias_method, :write, WRITE_WITHOUT_DEV_UI)
|
173
|
+
end
|
174
|
+
|
175
|
+
def activate(stream, streamname)
|
176
|
+
writer = StdoutRouter::Writer.new(stream, streamname)
|
177
|
+
|
178
|
+
raise if stream.respond_to?(WRITE_WITHOUT_DEV_UI)
|
179
|
+
stream.singleton_class.send(:alias_method, WRITE_WITHOUT_DEV_UI, :write)
|
180
|
+
stream.define_singleton_method(:write) do |*args|
|
181
|
+
writer.write(*args)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
Dev::UI::StdoutRouter.enable
|