dev-ui 0.0.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.
@@ -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