gorails 0.1.1 → 0.1.4
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/CHANGELOG.md +18 -1
- data/Gemfile.lock +1 -6
- data/README.md +41 -12
- data/bin/update-deps +95 -0
- data/exe/gorails +2 -1
- data/gorails.gemspec +0 -2
- data/lib/gorails/commands/railsbytes.rb +45 -4
- data/lib/gorails/commands/version.rb +15 -0
- data/lib/gorails/commands.rb +2 -5
- data/lib/gorails/version.rb +1 -1
- data/lib/gorails.rb +11 -20
- data/vendor/deps/cli-kit/REVISION +1 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/definition.rb +301 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/evaluation.rb +237 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/parser/node.rb +131 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/parser.rb +128 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args/tokenizer.rb +132 -0
- data/vendor/deps/cli-kit/lib/cli/kit/args.rb +15 -0
- data/vendor/deps/cli-kit/lib/cli/kit/base_command.rb +29 -0
- data/vendor/deps/cli-kit/lib/cli/kit/command_help.rb +256 -0
- data/vendor/deps/cli-kit/lib/cli/kit/command_registry.rb +141 -0
- data/vendor/deps/cli-kit/lib/cli/kit/config.rb +137 -0
- data/vendor/deps/cli-kit/lib/cli/kit/core_ext.rb +30 -0
- data/vendor/deps/cli-kit/lib/cli/kit/error_handler.rb +165 -0
- data/vendor/deps/cli-kit/lib/cli/kit/executor.rb +99 -0
- data/vendor/deps/cli-kit/lib/cli/kit/ini.rb +94 -0
- data/vendor/deps/cli-kit/lib/cli/kit/levenshtein.rb +89 -0
- data/vendor/deps/cli-kit/lib/cli/kit/logger.rb +95 -0
- data/vendor/deps/cli-kit/lib/cli/kit/opts.rb +284 -0
- data/vendor/deps/cli-kit/lib/cli/kit/resolver.rb +67 -0
- data/vendor/deps/cli-kit/lib/cli/kit/sorbet_runtime_stub.rb +142 -0
- data/vendor/deps/cli-kit/lib/cli/kit/support/test_helper.rb +253 -0
- data/vendor/deps/cli-kit/lib/cli/kit/support.rb +10 -0
- data/vendor/deps/cli-kit/lib/cli/kit/system.rb +350 -0
- data/vendor/deps/cli-kit/lib/cli/kit/util.rb +133 -0
- data/vendor/deps/cli-kit/lib/cli/kit/version.rb +7 -0
- data/vendor/deps/cli-kit/lib/cli/kit.rb +151 -0
- data/vendor/deps/cli-ui/REVISION +1 -0
- data/vendor/deps/cli-ui/lib/cli/ui/ansi.rb +180 -0
- data/vendor/deps/cli-ui/lib/cli/ui/color.rb +98 -0
- data/vendor/deps/cli-ui/lib/cli/ui/formatter.rb +216 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_stack.rb +116 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/box.rb +176 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +149 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame/frame_style.rb +112 -0
- data/vendor/deps/cli-ui/lib/cli/ui/frame.rb +300 -0
- data/vendor/deps/cli-ui/lib/cli/ui/glyph.rb +92 -0
- data/vendor/deps/cli-ui/lib/cli/ui/os.rb +58 -0
- data/vendor/deps/cli-ui/lib/cli/ui/printer.rb +72 -0
- data/vendor/deps/cli-ui/lib/cli/ui/progress.rb +102 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/interactive_options.rb +534 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt/options_handler.rb +36 -0
- data/vendor/deps/cli-ui/lib/cli/ui/prompt.rb +354 -0
- data/vendor/deps/cli-ui/lib/cli/ui/sorbet_runtime_stub.rb +143 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner/async.rb +46 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner/spin_group.rb +292 -0
- data/vendor/deps/cli-ui/lib/cli/ui/spinner.rb +82 -0
- data/vendor/deps/cli-ui/lib/cli/ui/stdout_router.rb +264 -0
- data/vendor/deps/cli-ui/lib/cli/ui/terminal.rb +53 -0
- data/vendor/deps/cli-ui/lib/cli/ui/truncater.rb +107 -0
- data/vendor/deps/cli-ui/lib/cli/ui/version.rb +6 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets/base.rb +37 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets/status.rb +75 -0
- data/vendor/deps/cli-ui/lib/cli/ui/widgets.rb +91 -0
- data/vendor/deps/cli-ui/lib/cli/ui/wrap.rb +63 -0
- data/vendor/deps/cli-ui/lib/cli/ui.rb +356 -0
- metadata +59 -30
@@ -0,0 +1,292 @@
|
|
1
|
+
# typed: true
|
2
|
+
module CLI
|
3
|
+
module UI
|
4
|
+
module Spinner
|
5
|
+
class SpinGroup
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
# Initializes a new spin group
|
9
|
+
# This lets you add +Task+ objects to the group to multi-thread work
|
10
|
+
#
|
11
|
+
# ==== Options
|
12
|
+
#
|
13
|
+
# * +:auto_debrief+ - Automatically debrief exceptions? Default to true
|
14
|
+
#
|
15
|
+
# ==== Example Usage
|
16
|
+
#
|
17
|
+
# CLI::UI::SpinGroup.new do |spin_group|
|
18
|
+
# spin_group.add('Title') { |spinner| sleep 3.0 }
|
19
|
+
# spin_group.add('Title 2') { |spinner| sleep 3.0; spinner.update_title('New Title'); sleep 3.0 }
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Output:
|
23
|
+
#
|
24
|
+
# https://user-images.githubusercontent.com/3074765/33798558-c452fa26-dce8-11e7-9e90-b4b34df21a46.gif
|
25
|
+
#
|
26
|
+
sig { params(auto_debrief: T::Boolean).void }
|
27
|
+
def initialize(auto_debrief: true)
|
28
|
+
@m = Mutex.new
|
29
|
+
@consumed_lines = 0
|
30
|
+
@tasks = []
|
31
|
+
@auto_debrief = auto_debrief
|
32
|
+
@start = Time.new
|
33
|
+
if block_given?
|
34
|
+
yield self
|
35
|
+
wait
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Task
|
40
|
+
extend T::Sig
|
41
|
+
|
42
|
+
sig { returns(String) }
|
43
|
+
attr_reader :title, :stdout, :stderr
|
44
|
+
|
45
|
+
sig { returns(T::Boolean) }
|
46
|
+
attr_reader :success
|
47
|
+
|
48
|
+
sig { returns(T.nilable(Exception)) }
|
49
|
+
attr_reader :exception
|
50
|
+
|
51
|
+
# Initializes a new Task
|
52
|
+
# This is managed entirely internally by +SpinGroup+
|
53
|
+
#
|
54
|
+
# ==== Attributes
|
55
|
+
#
|
56
|
+
# * +title+ - Title of the task
|
57
|
+
# * +block+ - Block for the task, will be provided with an instance of the spinner
|
58
|
+
#
|
59
|
+
sig { params(title: String, block: T.proc.params(task: Task).returns(T.untyped)).void }
|
60
|
+
def initialize(title, &block)
|
61
|
+
@title = title
|
62
|
+
@always_full_render = title =~ Formatter::SCAN_WIDGET
|
63
|
+
@thread = Thread.new do
|
64
|
+
cap = CLI::UI::StdoutRouter::Capture.new(with_frame_inset: false) { block.call(self) }
|
65
|
+
begin
|
66
|
+
cap.run
|
67
|
+
ensure
|
68
|
+
@stdout = cap.stdout
|
69
|
+
@stderr = cap.stderr
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
@m = Mutex.new
|
74
|
+
@force_full_render = false
|
75
|
+
@done = false
|
76
|
+
@exception = nil
|
77
|
+
@success = false
|
78
|
+
end
|
79
|
+
|
80
|
+
# Checks if a task is finished
|
81
|
+
#
|
82
|
+
sig { returns(T::Boolean) }
|
83
|
+
def check
|
84
|
+
return true if @done
|
85
|
+
return false if @thread.alive?
|
86
|
+
|
87
|
+
@done = true
|
88
|
+
begin
|
89
|
+
status = @thread.join.status
|
90
|
+
@success = (status == false)
|
91
|
+
@success = false if @thread.value == TASK_FAILED
|
92
|
+
rescue => exc
|
93
|
+
@exception = exc
|
94
|
+
@success = false
|
95
|
+
end
|
96
|
+
|
97
|
+
@done
|
98
|
+
end
|
99
|
+
|
100
|
+
# Re-renders the task if required:
|
101
|
+
#
|
102
|
+
# We try to be as lazy as possible in re-rendering the full line. The
|
103
|
+
# spinner rune will change on each render for the most part, but the
|
104
|
+
# body text will rarely have changed. If the body text *has* changed,
|
105
|
+
# we set @force_full_render.
|
106
|
+
#
|
107
|
+
# Further, if the title string includes any CLI::UI::Widgets, we
|
108
|
+
# assume that it may change from render to render, since those
|
109
|
+
# evaluate more dynamically than the rest of our format codes, which
|
110
|
+
# are just text formatters. This is controlled by @always_full_render.
|
111
|
+
#
|
112
|
+
# ==== Attributes
|
113
|
+
#
|
114
|
+
# * +index+ - index of the task
|
115
|
+
# * +force+ - force rerender of the task
|
116
|
+
# * +width+ - current terminal width to format for
|
117
|
+
#
|
118
|
+
sig { params(index: Integer, force: T::Boolean, width: Integer).returns(String) }
|
119
|
+
def render(index, force = true, width: CLI::UI::Terminal.width)
|
120
|
+
@m.synchronize do
|
121
|
+
if force || @always_full_render || @force_full_render
|
122
|
+
full_render(index, width)
|
123
|
+
else
|
124
|
+
partial_render(index)
|
125
|
+
end
|
126
|
+
ensure
|
127
|
+
@force_full_render = false
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Update the spinner title
|
132
|
+
#
|
133
|
+
# ==== Attributes
|
134
|
+
#
|
135
|
+
# * +title+ - title to change the spinner to
|
136
|
+
#
|
137
|
+
sig { params(new_title: String).void }
|
138
|
+
def update_title(new_title)
|
139
|
+
@m.synchronize do
|
140
|
+
@always_full_render = new_title =~ Formatter::SCAN_WIDGET
|
141
|
+
@title = new_title
|
142
|
+
@force_full_render = true
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
sig { params(index: Integer, terminal_width: Integer).returns(String) }
|
149
|
+
def full_render(index, terminal_width)
|
150
|
+
prefix = inset +
|
151
|
+
glyph(index) +
|
152
|
+
CLI::UI::Color::RESET.code +
|
153
|
+
' '
|
154
|
+
|
155
|
+
truncation_width = terminal_width - CLI::UI::ANSI.printing_width(prefix)
|
156
|
+
|
157
|
+
prefix +
|
158
|
+
CLI::UI.resolve_text(title, truncate_to: truncation_width) +
|
159
|
+
"\e[K"
|
160
|
+
end
|
161
|
+
|
162
|
+
sig { params(index: Integer).returns(String) }
|
163
|
+
def partial_render(index)
|
164
|
+
CLI::UI::ANSI.cursor_forward(inset_width) + glyph(index) + CLI::UI::Color::RESET.code
|
165
|
+
end
|
166
|
+
|
167
|
+
sig { params(index: Integer).returns(String) }
|
168
|
+
def glyph(index)
|
169
|
+
if @done
|
170
|
+
@success ? CLI::UI::Glyph::CHECK.to_s : CLI::UI::Glyph::X.to_s
|
171
|
+
else
|
172
|
+
GLYPHS[index]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
sig { returns(String) }
|
177
|
+
def inset
|
178
|
+
@inset ||= CLI::UI::Frame.prefix
|
179
|
+
end
|
180
|
+
|
181
|
+
sig { returns(Integer) }
|
182
|
+
def inset_width
|
183
|
+
@inset_width ||= CLI::UI::ANSI.printing_width(inset)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Add a new task
|
188
|
+
#
|
189
|
+
# ==== Attributes
|
190
|
+
#
|
191
|
+
# * +title+ - Title of the task
|
192
|
+
# * +block+ - Block for the task, will be provided with an instance of the spinner
|
193
|
+
#
|
194
|
+
# ==== Example Usage:
|
195
|
+
# spin_group = CLI::UI::SpinGroup.new
|
196
|
+
# spin_group.add('Title') { |spinner| sleep 1.0 }
|
197
|
+
# spin_group.wait
|
198
|
+
#
|
199
|
+
sig { params(title: String, block: T.proc.params(task: Task).void).void }
|
200
|
+
def add(title, &block)
|
201
|
+
@m.synchronize do
|
202
|
+
@tasks << Task.new(title, &block)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Tells the group you're done adding tasks and to wait for all of them to finish
|
207
|
+
#
|
208
|
+
# ==== Example Usage:
|
209
|
+
# spin_group = CLI::UI::SpinGroup.new
|
210
|
+
# spin_group.add('Title') { |spinner| sleep 1.0 }
|
211
|
+
# spin_group.wait
|
212
|
+
#
|
213
|
+
sig { returns(T::Boolean) }
|
214
|
+
def wait
|
215
|
+
idx = 0
|
216
|
+
|
217
|
+
loop do
|
218
|
+
all_done = T.let(true, T::Boolean)
|
219
|
+
|
220
|
+
width = CLI::UI::Terminal.width
|
221
|
+
|
222
|
+
@m.synchronize do
|
223
|
+
CLI::UI.raw do
|
224
|
+
@tasks.each.with_index do |task, int_index|
|
225
|
+
nat_index = int_index + 1
|
226
|
+
task_done = task.check
|
227
|
+
all_done = false unless task_done
|
228
|
+
|
229
|
+
if nat_index > @consumed_lines
|
230
|
+
print(task.render(idx, true, width: width) + "\n")
|
231
|
+
@consumed_lines += 1
|
232
|
+
else
|
233
|
+
offset = @consumed_lines - int_index
|
234
|
+
move_to = CLI::UI::ANSI.cursor_up(offset) + "\r"
|
235
|
+
move_from = "\r" + CLI::UI::ANSI.cursor_down(offset)
|
236
|
+
|
237
|
+
print(move_to + task.render(idx, idx.zero?, width: width) + move_from)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
break if all_done
|
244
|
+
|
245
|
+
idx = (idx + 1) % GLYPHS.size
|
246
|
+
Spinner.index = idx
|
247
|
+
sleep(PERIOD)
|
248
|
+
end
|
249
|
+
|
250
|
+
if @auto_debrief
|
251
|
+
debrief
|
252
|
+
else
|
253
|
+
@m.synchronize do
|
254
|
+
@tasks.all?(&:success)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Debriefs failed tasks is +auto_debrief+ is true
|
260
|
+
#
|
261
|
+
sig { returns(T::Boolean) }
|
262
|
+
def debrief
|
263
|
+
@m.synchronize do
|
264
|
+
@tasks.each do |task|
|
265
|
+
next if task.success
|
266
|
+
|
267
|
+
e = task.exception
|
268
|
+
out = task.stdout
|
269
|
+
err = task.stderr
|
270
|
+
|
271
|
+
CLI::UI::Frame.open('Task Failed: ' + task.title, color: :red, timing: Time.new - @start) do
|
272
|
+
if e
|
273
|
+
puts "#{e.class}: #{e.message}"
|
274
|
+
puts "\tfrom #{e.backtrace.join("\n\tfrom ")}"
|
275
|
+
end
|
276
|
+
|
277
|
+
CLI::UI::Frame.divider('STDOUT')
|
278
|
+
out = '(empty)' if out.nil? || out.strip.empty?
|
279
|
+
puts out
|
280
|
+
|
281
|
+
CLI::UI::Frame.divider('STDERR')
|
282
|
+
err = '(empty)' if err.nil? || err.strip.empty?
|
283
|
+
puts err
|
284
|
+
end
|
285
|
+
end
|
286
|
+
@tasks.all?(&:success)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# typed: true
|
2
|
+
# frozen-string-literal: true
|
3
|
+
|
4
|
+
require 'cli/ui'
|
5
|
+
|
6
|
+
module CLI
|
7
|
+
module UI
|
8
|
+
module Spinner
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
autoload :Async, 'cli/ui/spinner/async'
|
12
|
+
autoload :SpinGroup, 'cli/ui/spinner/spin_group'
|
13
|
+
|
14
|
+
PERIOD = 0.1 # seconds
|
15
|
+
TASK_FAILED = :task_failed
|
16
|
+
|
17
|
+
RUNES = if CLI::UI::OS.current.use_emoji?
|
18
|
+
['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].freeze
|
19
|
+
else
|
20
|
+
['\\', '|', '/', '-', '\\', '|', '/', '-'].freeze
|
21
|
+
end
|
22
|
+
|
23
|
+
colors = [CLI::UI::Color::CYAN.code] * (RUNES.size / 2).ceil +
|
24
|
+
[CLI::UI::Color::MAGENTA.code] * (RUNES.size / 2).to_i
|
25
|
+
GLYPHS = colors.zip(RUNES).map(&:join)
|
26
|
+
|
27
|
+
class << self
|
28
|
+
extend T::Sig
|
29
|
+
|
30
|
+
sig { returns(T.nilable(Integer)) }
|
31
|
+
attr_accessor(:index)
|
32
|
+
|
33
|
+
# We use this from CLI::UI::Widgets::Status to render an additional
|
34
|
+
# spinner next to the "working" element. While this global state looks
|
35
|
+
# a bit repulsive at first, it's worth realizing that:
|
36
|
+
#
|
37
|
+
# * It's managed by the SpinGroup#wait method, not individual tasks; and
|
38
|
+
# * It would be complete insanity to run two separate but concurrent SpinGroups.
|
39
|
+
#
|
40
|
+
# While it would be possible to stitch through some connection between
|
41
|
+
# the SpinGroup and the Widgets included in its title, this is simpler
|
42
|
+
# in practice and seems unlikely to cause issues in practice.
|
43
|
+
sig { returns(String) }
|
44
|
+
def current_rune
|
45
|
+
RUNES[index || 0]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Adds a single spinner
|
50
|
+
# Uses an interactive session to allow the user to pick an answer
|
51
|
+
# Can use arrows, y/n, numbers (1/2), and vim bindings to control
|
52
|
+
#
|
53
|
+
# https://user-images.githubusercontent.com/3074765/33798295-d94fd822-dce3-11e7-819b-43e5502d490e.gif
|
54
|
+
#
|
55
|
+
# ==== Attributes
|
56
|
+
#
|
57
|
+
# * +title+ - Title of the spinner to use
|
58
|
+
#
|
59
|
+
# ==== Options
|
60
|
+
#
|
61
|
+
# * +:auto_debrief+ - Automatically debrief exceptions? Default to true
|
62
|
+
#
|
63
|
+
# ==== Block
|
64
|
+
#
|
65
|
+
# * *spinner+ - Instance of the spinner. Can call +update_title+ to update the user of changes
|
66
|
+
#
|
67
|
+
# ==== Example Usage:
|
68
|
+
#
|
69
|
+
# CLI::UI::Spinner.spin('Title') { sleep 1.0 }
|
70
|
+
#
|
71
|
+
sig do
|
72
|
+
params(title: String, auto_debrief: T::Boolean, block: T.proc.params(task: SpinGroup::Task).void)
|
73
|
+
.returns(T::Boolean)
|
74
|
+
end
|
75
|
+
def self.spin(title, auto_debrief: true, &block)
|
76
|
+
sg = SpinGroup.new(auto_debrief: auto_debrief)
|
77
|
+
sg.add(title, &block)
|
78
|
+
sg.wait
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,264 @@
|
|
1
|
+
# typed: true
|
2
|
+
require 'cli/ui'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module CLI
|
6
|
+
module UI
|
7
|
+
module StdoutRouter
|
8
|
+
class Writer
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(stream: IOLike, name: Symbol).void }
|
12
|
+
def initialize(stream, name)
|
13
|
+
@stream = stream
|
14
|
+
@name = name
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(args: String).void }
|
18
|
+
def write(*args)
|
19
|
+
args = args.map do |str|
|
20
|
+
if auto_frame_inset?
|
21
|
+
str = str.dup # unfreeze
|
22
|
+
str = str.force_encoding(Encoding::UTF_8)
|
23
|
+
apply_line_prefix(str, CLI::UI::Frame.prefix)
|
24
|
+
else
|
25
|
+
@pending_newline = false
|
26
|
+
str
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# hook return of false suppresses output.
|
31
|
+
if (hook = Thread.current[:cliui_output_hook])
|
32
|
+
return if hook.call(args.map(&:to_s).join, @name) == false
|
33
|
+
end
|
34
|
+
|
35
|
+
T.unsafe(@stream).write_without_cli_ui(*prepend_id(@stream, args))
|
36
|
+
if (dup = StdoutRouter.duplicate_output_to)
|
37
|
+
T.unsafe(dup).write(*prepend_id(dup, args))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
sig { params(stream: IOLike, args: T::Array[String]).returns(T::Array[String]) }
|
44
|
+
def prepend_id(stream, args)
|
45
|
+
return args unless prepend_id_for_stream(stream)
|
46
|
+
|
47
|
+
args.map do |a|
|
48
|
+
next a if a.chomp.empty? # allow new lines to be new lines
|
49
|
+
|
50
|
+
"[#{Thread.current[:cliui_output_id][:id]}] #{a}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { params(stream: IOLike).returns(T::Boolean) }
|
55
|
+
def prepend_id_for_stream(stream)
|
56
|
+
return false unless Thread.current[:cliui_output_id]
|
57
|
+
return true if Thread.current[:cliui_output_id][:streams].include?(stream)
|
58
|
+
|
59
|
+
false
|
60
|
+
end
|
61
|
+
|
62
|
+
sig { returns(T::Boolean) }
|
63
|
+
def auto_frame_inset?
|
64
|
+
!Thread.current[:no_cliui_frame_inset]
|
65
|
+
end
|
66
|
+
|
67
|
+
sig { params(str: String, prefix: String).returns(String) }
|
68
|
+
def apply_line_prefix(str, prefix)
|
69
|
+
return '' if str.empty?
|
70
|
+
|
71
|
+
prefixed = +''
|
72
|
+
str.force_encoding(Encoding::UTF_8).lines.each do |line|
|
73
|
+
if @pending_newline
|
74
|
+
prefixed << line
|
75
|
+
@pending_newline = false
|
76
|
+
else
|
77
|
+
prefixed << prefix << line
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@pending_newline = !str.end_with?("\n")
|
81
|
+
prefixed
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Capture
|
86
|
+
extend T::Sig
|
87
|
+
|
88
|
+
@m = Mutex.new
|
89
|
+
@active_captures = 0
|
90
|
+
@saved_stdin = nil
|
91
|
+
|
92
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
93
|
+
def self.with_stdin_masked(&block)
|
94
|
+
@m.synchronize do
|
95
|
+
if @active_captures.zero?
|
96
|
+
@saved_stdin = $stdin
|
97
|
+
$stdin, w = IO.pipe
|
98
|
+
$stdin.close
|
99
|
+
w.close
|
100
|
+
end
|
101
|
+
@active_captures += 1
|
102
|
+
end
|
103
|
+
|
104
|
+
yield
|
105
|
+
ensure
|
106
|
+
@m.synchronize do
|
107
|
+
@active_captures -= 1
|
108
|
+
if @active_captures.zero?
|
109
|
+
$stdin = @saved_stdin
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
sig do
|
115
|
+
params(with_frame_inset: T::Boolean, block: T.proc.void).void
|
116
|
+
end
|
117
|
+
def initialize(with_frame_inset: true, &block)
|
118
|
+
@with_frame_inset = with_frame_inset
|
119
|
+
@block = block
|
120
|
+
@stdout = ''
|
121
|
+
@stderr = ''
|
122
|
+
end
|
123
|
+
|
124
|
+
sig { returns(String) }
|
125
|
+
attr_reader :stdout, :stderr
|
126
|
+
|
127
|
+
sig { returns(T.untyped) }
|
128
|
+
def run
|
129
|
+
require 'stringio'
|
130
|
+
|
131
|
+
StdoutRouter.assert_enabled!
|
132
|
+
|
133
|
+
out = StringIO.new
|
134
|
+
err = StringIO.new
|
135
|
+
|
136
|
+
prev_frame_inset = Thread.current[:no_cliui_frame_inset]
|
137
|
+
prev_hook = Thread.current[:cliui_output_hook]
|
138
|
+
|
139
|
+
if Thread.current.respond_to?(:report_on_exception)
|
140
|
+
Thread.current.report_on_exception = false
|
141
|
+
end
|
142
|
+
|
143
|
+
self.class.with_stdin_masked do
|
144
|
+
Thread.current[:no_cliui_frame_inset] = !@with_frame_inset
|
145
|
+
Thread.current[:cliui_output_hook] = ->(data, stream) do
|
146
|
+
case stream
|
147
|
+
when :stdout then out.write(data)
|
148
|
+
when :stderr then err.write(data)
|
149
|
+
else raise
|
150
|
+
end
|
151
|
+
false # suppress writing to terminal
|
152
|
+
end
|
153
|
+
|
154
|
+
begin
|
155
|
+
@block.call
|
156
|
+
ensure
|
157
|
+
@stdout = out.string
|
158
|
+
@stderr = err.string
|
159
|
+
end
|
160
|
+
end
|
161
|
+
ensure
|
162
|
+
Thread.current[:cliui_output_hook] = prev_hook
|
163
|
+
Thread.current[:no_cliui_frame_inset] = prev_frame_inset
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
class << self
|
168
|
+
extend T::Sig
|
169
|
+
|
170
|
+
WRITE_WITHOUT_CLI_UI = :write_without_cli_ui
|
171
|
+
|
172
|
+
NotEnabled = Class.new(StandardError)
|
173
|
+
|
174
|
+
sig { returns(T.nilable(IOLike)) }
|
175
|
+
attr_accessor :duplicate_output_to
|
176
|
+
|
177
|
+
sig do
|
178
|
+
type_parameters(:T)
|
179
|
+
.params(on_streams: T::Array[IOLike], block: T.proc.params(id: String).returns(T.type_parameter(:T)))
|
180
|
+
.returns(T.type_parameter(:T))
|
181
|
+
end
|
182
|
+
def with_id(on_streams:, &block)
|
183
|
+
require 'securerandom'
|
184
|
+
id = format('%05d', rand(10**5))
|
185
|
+
Thread.current[:cliui_output_id] = {
|
186
|
+
id: id,
|
187
|
+
streams: on_streams.map { |stream| T.cast(stream, IOLike) },
|
188
|
+
}
|
189
|
+
yield(id)
|
190
|
+
ensure
|
191
|
+
Thread.current[:cliui_output_id] = nil
|
192
|
+
end
|
193
|
+
|
194
|
+
sig { returns(T.nilable(T::Hash[Symbol, T.any(String, IOLike)])) }
|
195
|
+
def current_id
|
196
|
+
Thread.current[:cliui_output_id]
|
197
|
+
end
|
198
|
+
|
199
|
+
sig { void }
|
200
|
+
def assert_enabled!
|
201
|
+
raise NotEnabled unless enabled?
|
202
|
+
end
|
203
|
+
|
204
|
+
sig { type_parameters(:T).params(block: T.proc.returns(T.type_parameter(:T))).returns(T.type_parameter(:T)) }
|
205
|
+
def with_enabled(&block)
|
206
|
+
enable
|
207
|
+
yield
|
208
|
+
ensure
|
209
|
+
disable
|
210
|
+
end
|
211
|
+
|
212
|
+
# TODO: remove this
|
213
|
+
sig { void }
|
214
|
+
def ensure_activated
|
215
|
+
enable unless enabled?
|
216
|
+
end
|
217
|
+
|
218
|
+
sig { returns(T::Boolean) }
|
219
|
+
def enable
|
220
|
+
return false if enabled?($stdout) || enabled?($stderr)
|
221
|
+
|
222
|
+
activate($stdout, :stdout)
|
223
|
+
activate($stderr, :stderr)
|
224
|
+
true
|
225
|
+
end
|
226
|
+
|
227
|
+
sig { params(stream: IOLike).returns(T::Boolean) }
|
228
|
+
def enabled?(stream = $stdout)
|
229
|
+
stream.respond_to?(WRITE_WITHOUT_CLI_UI)
|
230
|
+
end
|
231
|
+
|
232
|
+
sig { returns(T::Boolean) }
|
233
|
+
def disable
|
234
|
+
return false unless enabled?($stdout) && enabled?($stderr)
|
235
|
+
|
236
|
+
deactivate($stdout)
|
237
|
+
deactivate($stderr)
|
238
|
+
true
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
|
243
|
+
sig { params(stream: IOLike).void }
|
244
|
+
def deactivate(stream)
|
245
|
+
sc = stream.singleton_class
|
246
|
+
sc.send(:remove_method, :write)
|
247
|
+
sc.send(:alias_method, :write, WRITE_WITHOUT_CLI_UI)
|
248
|
+
end
|
249
|
+
|
250
|
+
sig { params(stream: IOLike, streamname: Symbol).void }
|
251
|
+
def activate(stream, streamname)
|
252
|
+
writer = StdoutRouter::Writer.new(stream, streamname)
|
253
|
+
|
254
|
+
raise if stream.respond_to?(WRITE_WITHOUT_CLI_UI)
|
255
|
+
|
256
|
+
stream.singleton_class.send(:alias_method, WRITE_WITHOUT_CLI_UI, :write)
|
257
|
+
stream.define_singleton_method(:write) do |*args|
|
258
|
+
writer.write(*args)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# typed: true
|
2
|
+
require 'cli/ui'
|
3
|
+
require 'io/console'
|
4
|
+
|
5
|
+
module CLI
|
6
|
+
module UI
|
7
|
+
module Terminal
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
DEFAULT_WIDTH = 80
|
11
|
+
DEFAULT_HEIGHT = 24
|
12
|
+
|
13
|
+
# Returns the width of the terminal, if possible
|
14
|
+
# Otherwise will return DEFAULT_WIDTH
|
15
|
+
#
|
16
|
+
sig { returns(Integer) }
|
17
|
+
def self.width
|
18
|
+
winsize[1]
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the width of the terminal, if possible
|
22
|
+
# Otherwise, will return DEFAULT_HEIGHT
|
23
|
+
#
|
24
|
+
sig { returns(Integer) }
|
25
|
+
def self.height
|
26
|
+
winsize[0]
|
27
|
+
end
|
28
|
+
|
29
|
+
sig { returns([Integer, Integer]) }
|
30
|
+
def self.winsize
|
31
|
+
@winsize ||= begin
|
32
|
+
winsize = IO.console.winsize
|
33
|
+
setup_winsize_trap
|
34
|
+
|
35
|
+
if winsize.any?(&:zero?)
|
36
|
+
[DEFAULT_HEIGHT, DEFAULT_WIDTH]
|
37
|
+
else
|
38
|
+
winsize
|
39
|
+
end
|
40
|
+
rescue
|
41
|
+
[DEFAULT_HEIGHT, DEFAULT_WIDTH]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { void }
|
46
|
+
def self.setup_winsize_trap
|
47
|
+
@winsize_trap ||= Signal.trap('WINCH') do
|
48
|
+
@winsize = nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|