rspec-capturing-formatter 1.0.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +111 -0
- data/Rakefile +80 -0
- data/lib/rspec/capturing_formatter/configuration.rb +77 -0
- data/lib/rspec/capturing_formatter/renderer.rb +457 -0
- data/lib/rspec/capturing_formatter/sanitizer.rb +378 -0
- data/lib/rspec/capturing_formatter/stream_proxy.rb +414 -0
- data/lib/rspec/capturing_formatter/version.rb +9 -0
- data/lib/rspec/capturing_formatter/windows_command_line.rb +65 -0
- data/lib/rspec/capturing_formatter/windows_terminal.rb +87 -0
- data/lib/rspec/capturing_formatter.rb +319 -0
- metadata +120 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module RSpec
|
|
6
|
+
class CapturingFormatter
|
|
7
|
+
# Owns process-global stream capture and serializes captured writes with formatter callbacks.
|
|
8
|
+
class CaptureManager
|
|
9
|
+
# Older supported Rubies need keyword_init: true to construct leases from keyword arguments.
|
|
10
|
+
# standard:disable Style/RedundantStructKeywordInit
|
|
11
|
+
Lease = Struct.new(:manager, :owner, :generation, keyword_init: true) do
|
|
12
|
+
# standard:enable Style/RedundantStructKeywordInit
|
|
13
|
+
def owner?
|
|
14
|
+
owner
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def instance
|
|
20
|
+
@instance ||= new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def install!
|
|
24
|
+
instance.install!
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attr_reader :stdout_proxy, :stderr_proxy
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
# Renderer writes can re-enter through a proxy, so capture synchronization must be reentrant.
|
|
32
|
+
@monitor = Monitor.new
|
|
33
|
+
@installed = false
|
|
34
|
+
@active = false
|
|
35
|
+
@pending_nonblock = {}
|
|
36
|
+
@generation = 0
|
|
37
|
+
@bypass_key = :rspec_capturing_formatter_bypass_depth
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def install!
|
|
41
|
+
@monitor.synchronize do
|
|
42
|
+
return self if @installed && $stdout.equal?(@stdout_proxy) && $stderr.equal?(@stderr_proxy)
|
|
43
|
+
|
|
44
|
+
@original_stdout ||= $stdout
|
|
45
|
+
@original_stderr ||= $stderr
|
|
46
|
+
@stdout_proxy ||= StreamProxy.new(@original_stdout, :stdout, self)
|
|
47
|
+
@stderr_proxy ||= StreamProxy.new(@original_stderr, :stderr, self)
|
|
48
|
+
$stdout = @stdout_proxy
|
|
49
|
+
$stderr = @stderr_proxy
|
|
50
|
+
@installed = true
|
|
51
|
+
end
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def activate(output, formatter)
|
|
56
|
+
existing = @monitor.synchronize { active_lease }
|
|
57
|
+
return existing if existing
|
|
58
|
+
|
|
59
|
+
install!
|
|
60
|
+
@monitor.synchronize do
|
|
61
|
+
existing = active_lease
|
|
62
|
+
return existing if existing
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
@formatter = formatter
|
|
66
|
+
@active = true
|
|
67
|
+
@generation += 1
|
|
68
|
+
@stdout_proxy.activate!
|
|
69
|
+
@stderr_proxy.activate!
|
|
70
|
+
Lease.new(manager: self, owner: true, generation: @generation)
|
|
71
|
+
# Activation mutates process-global capture state, so roll it back even outside StandardError.
|
|
72
|
+
# standard:disable Lint/RescueException
|
|
73
|
+
rescue Exception
|
|
74
|
+
# standard:enable Lint/RescueException
|
|
75
|
+
@active = false
|
|
76
|
+
@formatter = nil
|
|
77
|
+
@stdout_proxy.deactivate!
|
|
78
|
+
@stderr_proxy.deactivate!
|
|
79
|
+
restore_globals
|
|
80
|
+
raise
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def active?
|
|
86
|
+
@monitor.synchronize { @active }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def generation
|
|
90
|
+
@monitor.synchronize { @generation }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def synchronize(&block)
|
|
94
|
+
@monitor.synchronize(&block)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def handle_write(proxy, value, method = :write, **options)
|
|
98
|
+
@monitor.synchronize do
|
|
99
|
+
pending = @pending_nonblock[proxy]
|
|
100
|
+
if pending
|
|
101
|
+
if nonblocking_method?(method) && pending != value
|
|
102
|
+
raise Errno::EAGAIN
|
|
103
|
+
end
|
|
104
|
+
# This payload is already captured; remove its marker while a bypassed flush may re-enter here.
|
|
105
|
+
@pending_nonblock.delete(proxy)
|
|
106
|
+
begin
|
|
107
|
+
@formatter.flush_pending
|
|
108
|
+
rescue IO::WaitWritable, Errno::EAGAIN
|
|
109
|
+
@pending_nonblock[proxy] = pending
|
|
110
|
+
raise
|
|
111
|
+
end
|
|
112
|
+
return value.bytesize if nonblocking_method?(method) && pending == value
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if !@active || bypassing? || proxy.raw_mode?
|
|
116
|
+
proxy.write_backing(value, method, **options)
|
|
117
|
+
else
|
|
118
|
+
begin
|
|
119
|
+
@formatter.capture(proxy.source, value, value.encoding)
|
|
120
|
+
rescue IO::WaitWritable, Errno::EAGAIN
|
|
121
|
+
@pending_nonblock[proxy] = value if nonblocking_method?(method)
|
|
122
|
+
raise
|
|
123
|
+
end
|
|
124
|
+
value.bytesize
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def bypass
|
|
130
|
+
depth = Thread.current[@bypass_key].to_i
|
|
131
|
+
Thread.current[@bypass_key] = depth + 1
|
|
132
|
+
yield
|
|
133
|
+
ensure
|
|
134
|
+
Thread.current[@bypass_key] = depth
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def bypassing?
|
|
138
|
+
Thread.current[@bypass_key].to_i.positive?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def deactivate(lease)
|
|
142
|
+
return unless lease&.owner?
|
|
143
|
+
return unless lease.manager.equal?(self)
|
|
144
|
+
|
|
145
|
+
@monitor.synchronize do
|
|
146
|
+
return unless @active && lease.generation == @generation
|
|
147
|
+
begin
|
|
148
|
+
@formatter.finish_capture
|
|
149
|
+
ensure
|
|
150
|
+
@active = false
|
|
151
|
+
@formatter = nil
|
|
152
|
+
@pending_nonblock.clear
|
|
153
|
+
@stdout_proxy.deactivate!
|
|
154
|
+
@stderr_proxy.deactivate!
|
|
155
|
+
restore_globals
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def restore!
|
|
161
|
+
@monitor.synchronize do
|
|
162
|
+
@active = false
|
|
163
|
+
@formatter = nil
|
|
164
|
+
@pending_nonblock.clear
|
|
165
|
+
@stdout_proxy&.deactivate!
|
|
166
|
+
@stderr_proxy&.deactivate!
|
|
167
|
+
restore_globals
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def rollback_if_unchanged(expected_generation)
|
|
172
|
+
@monitor.synchronize do
|
|
173
|
+
return if @active || @generation != expected_generation
|
|
174
|
+
|
|
175
|
+
restore_globals
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def uninstall!
|
|
180
|
+
restore!
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def nonblocking_method?(method)
|
|
186
|
+
method == :write_nonblock || method == :syswrite
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def active_lease
|
|
190
|
+
return unless @active
|
|
191
|
+
|
|
192
|
+
raise "another RSpec::CapturingFormatter is already active"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def restore_globals
|
|
196
|
+
# Do not overwrite a global stream that another component replaced while capture was active.
|
|
197
|
+
$stdout = @original_stdout if @stdout_proxy && $stdout.equal?(@stdout_proxy)
|
|
198
|
+
$stderr = @original_stderr if @stderr_proxy && $stderr.equal?(@stderr_proxy)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Presents an IO-like stream whose writes are captured or passed unchanged to its backing stream.
|
|
203
|
+
class StreamProxy
|
|
204
|
+
attr_reader :source
|
|
205
|
+
|
|
206
|
+
def initialize(backing, source, manager)
|
|
207
|
+
@backing = backing
|
|
208
|
+
@source = source
|
|
209
|
+
@manager = manager
|
|
210
|
+
@raw_mode = false
|
|
211
|
+
@closed = false
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def initialize_copy(other)
|
|
215
|
+
super
|
|
216
|
+
@manager = other.manager
|
|
217
|
+
# RSpec's any-process output matcher reopens from a clone and needs its current backing stream.
|
|
218
|
+
@manager.synchronize do
|
|
219
|
+
@backing = begin
|
|
220
|
+
other.backing.dup
|
|
221
|
+
rescue
|
|
222
|
+
other.backing
|
|
223
|
+
end
|
|
224
|
+
@raw_mode = other.raw_mode?
|
|
225
|
+
@closed = false
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def raw_mode?
|
|
230
|
+
@raw_mode
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
attr_reader :manager
|
|
234
|
+
|
|
235
|
+
attr_reader :backing
|
|
236
|
+
|
|
237
|
+
def activate!
|
|
238
|
+
@raw_mode = false
|
|
239
|
+
@closed = false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def deactivate!
|
|
243
|
+
@raw_mode = true
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def write(value)
|
|
247
|
+
string = String(value)
|
|
248
|
+
@manager.handle_write(self, string, :write)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def write_nonblock(value, exception: true)
|
|
252
|
+
string = String(value)
|
|
253
|
+
@manager.handle_write(self, string, :write_nonblock, exception: exception)
|
|
254
|
+
rescue IO::WaitWritable, Errno::EAGAIN
|
|
255
|
+
raise if exception
|
|
256
|
+
|
|
257
|
+
:wait_writable
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def syswrite(value)
|
|
261
|
+
string = String(value)
|
|
262
|
+
@manager.handle_write(self, string, :syswrite)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def <<(value)
|
|
266
|
+
write(value)
|
|
267
|
+
self
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def puts(*values)
|
|
271
|
+
values = [nil] if values.empty?
|
|
272
|
+
values.each { |value| put_value(value) }
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def put_value(value, ancestors = [])
|
|
277
|
+
if value.is_a?(Array)
|
|
278
|
+
if ancestors.include?(value.object_id)
|
|
279
|
+
write("[...]\n")
|
|
280
|
+
else
|
|
281
|
+
value.each { |entry| put_value(entry, ancestors + [value.object_id]) }
|
|
282
|
+
end
|
|
283
|
+
else
|
|
284
|
+
text = value.nil? ? "\n" : value.to_s
|
|
285
|
+
newline = "\n".encode(text.encoding)
|
|
286
|
+
write(text.end_with?(newline) ? text : text + newline)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def print(*values)
|
|
291
|
+
values = [$_] if values.empty? && defined?($_) && !$_.nil?
|
|
292
|
+
separator = $OUTPUT_FIELD_SEPARATOR
|
|
293
|
+
terminator = $OUTPUT_RECORD_SEPARATOR
|
|
294
|
+
values.each_with_index do |value, index|
|
|
295
|
+
write(separator) if index.positive? && separator
|
|
296
|
+
write(value)
|
|
297
|
+
end
|
|
298
|
+
write(terminator) if values.any? && terminator
|
|
299
|
+
nil
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def printf(format_string, *values)
|
|
303
|
+
write(format_string % values)
|
|
304
|
+
nil
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def putc(value)
|
|
308
|
+
character = value.is_a?(Integer) ? (value & 0xFF).chr(Encoding::BINARY) : value.to_s[0]
|
|
309
|
+
write(character)
|
|
310
|
+
value
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def flush
|
|
314
|
+
@manager.bypass { @backing.flush } if @backing.respond_to?(:flush)
|
|
315
|
+
self
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def sync
|
|
319
|
+
@backing.sync if @backing.respond_to?(:sync)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def sync=(value)
|
|
323
|
+
@backing.sync = value if @backing.respond_to?(:sync=)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def tty?
|
|
327
|
+
@backing.respond_to?(:tty?) && @backing.tty?
|
|
328
|
+
end
|
|
329
|
+
alias_method :isatty, :tty?
|
|
330
|
+
|
|
331
|
+
def closed?
|
|
332
|
+
@closed || (@backing.respond_to?(:closed?) && @backing.closed?)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def external_encoding
|
|
336
|
+
@backing.respond_to?(:external_encoding) ? @backing.external_encoding : Encoding::UTF_8
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def internal_encoding
|
|
340
|
+
@backing.respond_to?(:internal_encoding) ? @backing.internal_encoding : nil
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def set_encoding(*arguments)
|
|
344
|
+
@backing.set_encoding(*arguments) if @backing.respond_to?(:set_encoding)
|
|
345
|
+
self
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def binmode
|
|
349
|
+
@backing.binmode if @backing.respond_to?(:binmode)
|
|
350
|
+
self
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def fileno
|
|
354
|
+
@backing.fileno
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def to_io
|
|
358
|
+
self
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def reopen(target, *arguments)
|
|
362
|
+
# Proxy targets restore matcher snapshots; other targets must bypass capture.
|
|
363
|
+
@manager.synchronize do
|
|
364
|
+
if target.is_a?(StreamProxy)
|
|
365
|
+
if descriptor_backing?
|
|
366
|
+
@backing.reopen(target.backing)
|
|
367
|
+
else
|
|
368
|
+
@backing = target.backing
|
|
369
|
+
end
|
|
370
|
+
@raw_mode = target.raw_mode?
|
|
371
|
+
elsif descriptor_backing? && (target.respond_to?(:read) || target.respond_to?(:write))
|
|
372
|
+
@backing.reopen(target.respond_to?(:to_io) ? target.to_io : target, *arguments)
|
|
373
|
+
@raw_mode = true
|
|
374
|
+
elsif target.respond_to?(:read) || target.respond_to?(:write)
|
|
375
|
+
@backing = target
|
|
376
|
+
@raw_mode = true
|
|
377
|
+
elsif @backing.respond_to?(:reopen)
|
|
378
|
+
@backing.reopen(target, *arguments)
|
|
379
|
+
@raw_mode = true
|
|
380
|
+
else
|
|
381
|
+
@backing = target
|
|
382
|
+
@raw_mode = true
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
self
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def descriptor_backing?
|
|
389
|
+
defined?(IO) && @backing.is_a?(IO)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def close
|
|
393
|
+
@closed = true
|
|
394
|
+
# A retained proxy can outlive capture; do not close a backing stream restored as a process global.
|
|
395
|
+
@backing.close unless @backing.equal?($stdout) || @backing.equal?($stderr)
|
|
396
|
+
nil
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def write_backing(value, method = :write, **options)
|
|
400
|
+
@manager.bypass { @backing.public_send(method, value, **options) }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def method_missing(name, *arguments, &block)
|
|
404
|
+
return super unless @backing.respond_to?(name)
|
|
405
|
+
|
|
406
|
+
@backing.public_send(name, *arguments, &block)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def respond_to_missing?(name, include_private = false)
|
|
410
|
+
@backing.respond_to?(name, include_private) || super
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
class CapturingFormatter
|
|
5
|
+
# Builds rerun commands without exposing dynamic targets to cmd.exe parsing.
|
|
6
|
+
module WindowsCommandLine
|
|
7
|
+
DECODE_ARGUMENT = 'ARGV[0]=ARGV.fetch(0).unpack1("m0").force_encoding("UTF-8")'
|
|
8
|
+
RUN_RSPEC = 'load(Gem.bin_path("rspec-core","rspec"))'
|
|
9
|
+
|
|
10
|
+
# Protect characters that cmd.exe interprets while parsing a pasted command.
|
|
11
|
+
CMD_META = "()[]^\"`<>&|;, *?\t"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def rerun_command(value)
|
|
16
|
+
encoded_ruby_command(RUN_RSPEC, value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Dynamic values are encoded because cmd.exe expands paired percent signs
|
|
20
|
+
# and exclamation marks before an executable can receive them.
|
|
21
|
+
def encoded_ruby_command(script, value)
|
|
22
|
+
value = valid_value(value).encode(Encoding::UTF_8)
|
|
23
|
+
bootstrap = "#{DECODE_ARGUMENT};#{script}"
|
|
24
|
+
"ruby -e #{escape(bootstrap)} #{[value].pack("m0")}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def escape(value)
|
|
28
|
+
value = valid_value(value)
|
|
29
|
+
if value.match?(/[%!]/)
|
|
30
|
+
raise ArgumentError, "dynamic cmd.exe arguments containing percent signs or exclamation marks must be encoded"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Build Windows argv quoting before escaping the cmd.exe layer. A literal
|
|
34
|
+
# quote needs 2n + 1 backslashes; a closing quote after n backslashes needs 2n.
|
|
35
|
+
quoted = +'"'
|
|
36
|
+
backslashes = 0
|
|
37
|
+
value.each_char do |character|
|
|
38
|
+
if character == "\\"
|
|
39
|
+
backslashes += 1
|
|
40
|
+
elsif character == '"'
|
|
41
|
+
quoted << ("\\" * (backslashes * 2 + 1)) << '"'
|
|
42
|
+
backslashes = 0
|
|
43
|
+
else
|
|
44
|
+
quoted << ("\\" * backslashes) << character
|
|
45
|
+
backslashes = 0
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
quoted << ("\\" * (backslashes * 2)) << '"'
|
|
49
|
+
|
|
50
|
+
quoted.each_char.map do |character|
|
|
51
|
+
CMD_META.include?(character) ? "^#{character}" : character
|
|
52
|
+
end.join
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def valid_value(value)
|
|
56
|
+
value = value.to_s
|
|
57
|
+
if value.match?(/[\0\r\n]/)
|
|
58
|
+
raise ArgumentError, "Windows command arguments cannot contain NUL or newlines"
|
|
59
|
+
end
|
|
60
|
+
value
|
|
61
|
+
end
|
|
62
|
+
private_class_method :valid_value
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle" if Gem.win_platform?
|
|
4
|
+
|
|
5
|
+
module RSpec
|
|
6
|
+
class CapturingFormatter
|
|
7
|
+
# Determines ANSI support and enables Virtual Terminal processing for Windows Terminal output.
|
|
8
|
+
module WindowsTerminal
|
|
9
|
+
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
10
|
+
STD_OUTPUT_HANDLE = -11
|
|
11
|
+
STD_ERROR_HANDLE = -12
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Redirected output remains ANSI-capable; interactive output requires Windows Terminal and VT support.
|
|
15
|
+
# Fiddle failures fall back to plain text, while unrelated errors propagate.
|
|
16
|
+
def ansi_supported?(output, on_windows: Gem.win_platform?, env: ENV, api: nil)
|
|
17
|
+
return true unless on_windows
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
return true unless tty?(output)
|
|
21
|
+
return false unless windows_terminal?(env)
|
|
22
|
+
|
|
23
|
+
(api || NativeApi.new).enable_virtual_terminal_processing(output)
|
|
24
|
+
rescue Fiddle::Error
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def tty?(output)
|
|
32
|
+
output.respond_to?(:tty?) && output.tty?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def windows_terminal?(env)
|
|
36
|
+
value = env["WT_SESSION"]
|
|
37
|
+
value && !value.empty?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Isolates the Fiddle bindings for the Windows console API.
|
|
42
|
+
class NativeApi
|
|
43
|
+
def initialize
|
|
44
|
+
kernel32 = Fiddle.dlopen("kernel32.dll")
|
|
45
|
+
@get_std_handle = function(kernel32, "GetStdHandle", [Fiddle::TYPE_LONG], Fiddle::TYPE_VOIDP)
|
|
46
|
+
@get_console_mode = function(
|
|
47
|
+
kernel32,
|
|
48
|
+
"GetConsoleMode",
|
|
49
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
|
|
50
|
+
Fiddle::TYPE_INT
|
|
51
|
+
)
|
|
52
|
+
@set_console_mode = function(
|
|
53
|
+
kernel32,
|
|
54
|
+
"SetConsoleMode",
|
|
55
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG],
|
|
56
|
+
Fiddle::TYPE_INT
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def enable_virtual_terminal_processing(output)
|
|
61
|
+
handle = @get_std_handle.call(std_handle(output))
|
|
62
|
+
return false if handle.to_i.zero? || handle.to_i == -1
|
|
63
|
+
|
|
64
|
+
# Console modes are Windows DWORDs: four little-endian bytes.
|
|
65
|
+
mode = Fiddle::Pointer.malloc(4)
|
|
66
|
+
return false if @get_console_mode.call(handle, mode).zero?
|
|
67
|
+
|
|
68
|
+
current_mode = mode[0, 4].unpack1("V")
|
|
69
|
+
return true if (current_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING).positive?
|
|
70
|
+
|
|
71
|
+
!@set_console_mode.call(handle, current_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING).zero?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def function(library, name, arguments, result)
|
|
77
|
+
Fiddle::Function.new(library[name], arguments, result)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def std_handle(output)
|
|
81
|
+
source = output.source if output.respond_to?(:source)
|
|
82
|
+
(source == :stderr) ? STD_ERROR_HANDLE : STD_OUTPUT_HANDLE
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|