-
-
Notifications
You must be signed in to change notification settings - Fork 9
/
screen.rb
481 lines (430 loc) · 12 KB
/
screen.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# frozen_string_literal: true
begin
require "rbconfig"
rescue LoadError
end
require_relative "screen/version"
module TTY
# Responsible for detecting terminal screen size
#
# @api public
module Screen
# The Ruby configuration
#
# @return [Hash]
#
# @api private
RUBY_CONFIG = defined?(::RbConfig) ? ::RbConfig::CONFIG : {}
private_constant :RUBY_CONFIG
# Define module method as private
#
# @return [void]
#
# @api private
def self.private_module_function(name)
module_function(name)
private_class_method(name)
end
case RUBY_CONFIG["host_os"] || ::RUBY_PLATFORM
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
# Detect Windows system
#
# @return [Boolean]
#
# @api private
def windows?; true end
else
def windows?; false end
end
module_function :windows?
case RUBY_CONFIG["ruby_install_name"] || ::RUBY_ENGINE
when /jruby/
# Detect JRuby
#
# @return [Boolean]
#
# @api private
def jruby?; true end
else
def jruby?; false end
end
module_function :jruby?
# The default terminal screen size
#
# @return [Array(Integer, Integer)]
#
# @api private
DEFAULT_SIZE = [27, 80].freeze
@env = ENV
@output = $stderr
class << self
# The environment variables
#
# @example
# TTY::Screen.env
#
# @example
# TTY::Screen.env = {"ROWS" => "51", "COLUMNS" => "211"}
#
# @return [Enumerable]
#
# @api public
attr_accessor :env
# The output stream with standard error as default
#
# @example
# TTY::Screen.output
#
# @example
# TTY::Screen.output = $stdout
#
# @return [IO]
#
# @api public
attr_accessor :output
end
# Detect terminal screen size
#
# @example
# TTY::Screen.size # => [51, 211]
#
# @return [Array(Integer, Integer)]
# the terminal screen size
#
# @api public
def size(verbose: false)
size_from_java(verbose: verbose) ||
size_from_win_api(verbose: verbose) ||
size_from_ioctl ||
size_from_io_console(verbose: verbose) ||
size_from_readline(verbose: verbose) ||
size_from_tput ||
size_from_stty ||
size_from_env ||
size_from_ansicon ||
size_from_default
end
module_function :size
# Detect terminal screen width
#
# @example
# TTY::Screen.width # => 211
#
# @return [Integer]
#
# @api public
def width
size[1]
end
module_function :width
alias columns width
alias cols width
module_function :columns
module_function :cols
# Detect terminal screen height
#
# @example
# TTY::Screen.height # => 51
#
# @return [Integer]
#
# @api public
def height
size[0]
end
module_function :height
alias rows height
alias lines height
module_function :rows
module_function :lines
# Detect terminal screen size from default
#
# @return [Array(Integer, Integer)]
#
# @api private
def size_from_default
DEFAULT_SIZE
end
module_function :size_from_default
if windows?
# The standard output handle
#
# @return [Integer]
#
# @api private
STDOUT_HANDLE = 0xFFFFFFF5
# Detect terminal screen size from Windows API
#
# @param [Boolean] verbose
# the verbose mode, by default false
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_win_api(verbose: false)
require "fiddle" unless defined?(Fiddle)
kernel32 = Fiddle::Handle.new("kernel32")
get_std_handle = Fiddle::Function.new(
kernel32["GetStdHandle"], [-Fiddle::TYPE_INT], Fiddle::TYPE_INT)
get_console_buffer_info = Fiddle::Function.new(
kernel32["GetConsoleScreenBufferInfo"],
[Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
format = "SSSSSssssSS"
buffer = ([0] * format.size).pack(format)
stdout_handle = get_std_handle.(STDOUT_HANDLE)
get_console_buffer_info.(stdout_handle, buffer)
_, _, _, _, _, left, top, right, bottom, = buffer.unpack(format)
size = [bottom - top + 1, right - left + 1]
size if nonzero_column?(size[1] - 1)
rescue LoadError
warn "no native fiddle module found" if verbose
rescue Fiddle::DLError
# non windows platform or no kernel32 lib
end
else
def size_from_win_api(verbose: false); nil end
end
module_function :size_from_win_api
if jruby?
# Detect terminal screen size from Java
#
# @param [Boolean] verbose
# the verbose mode, by default false
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_java(verbose: false)
require "java"
java_import "jline.TerminalFactory"
terminal = TerminalFactory.get
size = [terminal.get_height, terminal.get_width]
size if nonzero_column?(size[1])
rescue
warn "failed to import java terminal package" if verbose
end
else
def size_from_java(verbose: false); nil end
end
module_function :size_from_java
# Detect terminal screen size from io-console
#
# On Windows, the io-console falls back to reading environment
# variables. This means any user changes to the terminal screen
# size will not be reflected in the runtime of the Ruby application.
#
# @param [Boolean] verbose
# the verbose mode, by default false
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_io_console(verbose: false)
return unless output.tty?
require "io/console" unless IO.method_defined?(:winsize)
return unless output.respond_to?(:winsize)
size = output.winsize
size if nonzero_column?(size[1])
rescue Errno::EOPNOTSUPP
# no support for winsize on output
rescue LoadError
warn "no native io/console support or io-console gem" if verbose
end
module_function :size_from_io_console
if !jruby? && output.respond_to?(:ioctl)
# The get window size control code for Linux
#
# @return [Integer]
#
# @api private
TIOCGWINSZ = 0x5413
# The get window size control code for FreeBSD and macOS
#
# @return [Integer]
#
# @api private
TIOCGWINSZ_PPC = 0x40087468
# The get window size control code for Solaris
#
# @return [Integer]
#
# @api private
TIOCGWINSZ_SOL = 0x5468
# The ioctl window size buffer format
#
# @return [String]
#
# @api private
TIOCGWINSZ_BUF_FMT = "SSSS"
private_constant :TIOCGWINSZ_BUF_FMT
# The ioctl window size buffer length
#
# @return [Integer]
#
# @api private
TIOCGWINSZ_BUF_LEN = TIOCGWINSZ_BUF_FMT.length
private_constant :TIOCGWINSZ_BUF_LEN
# Detect terminal screen size from ioctl
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_ioctl
buffer = Array.new(TIOCGWINSZ_BUF_LEN, 0).pack(TIOCGWINSZ_BUF_FMT)
if ioctl?(TIOCGWINSZ, buffer) ||
ioctl?(TIOCGWINSZ_PPC, buffer) ||
ioctl?(TIOCGWINSZ_SOL, buffer)
rows, cols, = buffer.unpack(TIOCGWINSZ_BUF_FMT)
[rows, cols] if nonzero_column?(cols)
end
end
# Check if the ioctl call gets window size on any standard stream
#
# @param [Integer] control
# the control code
# @param [String] buf
# the window size buffer
#
# @return [Boolean]
# true if the ioctl call gets window size, false otherwise
#
# @api private
def ioctl?(control, buf)
($stdout.ioctl(control, buf) >= 0) ||
($stdin.ioctl(control, buf) >= 0) ||
($stderr.ioctl(control, buf) >= 0)
rescue SystemCallError
false
end
module_function :ioctl?
else
def size_from_ioctl; nil end
end
module_function :size_from_ioctl
# Detect terminal screen size from readline
#
# @param [Boolean] verbose
# the verbose mode, by default false
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_readline(verbose: false)
return unless output.tty?
require "readline" unless defined?(::Readline)
return unless ::Readline.respond_to?(:get_screen_size)
size = ::Readline.get_screen_size
size if nonzero_column?(size[1])
rescue LoadError
warn "no readline gem" if verbose
rescue NotImplementedError
end
module_function :size_from_readline
# Detect terminal screen size from tput
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_tput
return unless output.tty? && command_exist?("tput")
lines = run_command("tput", "lines")
return unless lines
cols = run_command("tput", "cols")
[lines.to_i, cols.to_i] if nonzero_column?(cols)
end
module_function :size_from_tput
# Detect terminal screen size from stty
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_stty
return unless output.tty? && command_exist?("stty")
out = run_command("stty", "size")
return unless out
size = out.split.map(&:to_i)
size if nonzero_column?(size[1])
end
module_function :size_from_stty
# Detect terminal screen size from environment variables
#
# After executing Ruby code, when the user changes terminal
# screen size during code runtime, the code will not be
# notified, and hence will not see the new size reflected
# in its copy of LINES and COLUMNS environment variables.
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_env
return unless env["COLUMNS"] =~ /^\d+$/
size = [(env["LINES"] || env["ROWS"]).to_i, env["COLUMNS"].to_i]
size if nonzero_column?(size[1])
end
module_function :size_from_env
# Detect terminal screen size from the ANSICON environment variable
#
# @return [Array(Integer, Integer), nil]
# the terminal screen size or nil
#
# @api private
def size_from_ansicon
return unless env["ANSICON"] =~ /\((.*)x(.*)\)/
size = [::Regexp.last_match(2).to_i, ::Regexp.last_match(1).to_i]
size if nonzero_column?(size[1])
end
module_function :size_from_ansicon
# Check if a command exists
#
# @param [String] command
# the command to check
#
# @return [Boolean]
#
# @api private
def command_exist?(command)
exts = env.fetch("PATHEXT", "").split(::File::PATH_SEPARATOR)
env.fetch("PATH", "").split(::File::PATH_SEPARATOR).any? do |dir|
file = ::File.join(dir, command)
::File.exist?(file) ||
exts.any? { |ext| ::File.exist?("#{file}#{ext}") }
end
end
private_module_function :command_exist?
# Run command capturing the standard output
#
# @param [Array<String>] args
# the command arguments
#
# @return [String, nil]
# the command output or nil
#
# @api private
def run_command(*args)
%x(#{args.join(" ")})
rescue IOError, SystemCallError
nil
end
private_module_function :run_command
# Check if a number is non-zero
#
# @param [Integer, String] column
# the column to check
#
# @return [Boolean]
#
# @api private
def nonzero_column?(column)
column.to_i > 0
end
private_module_function :nonzero_column?
end # Screen
end # TTY