dev-ui 0.0.1

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