[ruby/reline] Overhaul io gate structure

(https://github.com/ruby/reline/pull/666)

* Overhaul IO gate structure

1. Move IO related classes to `lib/reline/io/` directory.
2. Rename `GeneralIO` to `Dumb`.
3. Use IO classes as instances instead of classes.

* Update lib/reline/io/ansi.rb

Co-authored-by: tomoya ishida <tomoyapenguin@gmail.com>

---------

https://github.com/ruby/reline/commit/dc1518e1ac

Co-authored-by: tomoya ishida <tomoyapenguin@gmail.com>
This commit is contained in:
Stan Lo 2024-06-01 11:28:03 +01:00 committed by git
parent 767aa0cdb6
commit cda69b5910
13 changed files with 367 additions and 339 deletions

View File

@ -7,6 +7,7 @@ require 'reline/key_stroke'
require 'reline/line_editor' require 'reline/line_editor'
require 'reline/history' require 'reline/history'
require 'reline/terminfo' require 'reline/terminfo'
require 'reline/io'
require 'reline/face' require 'reline/face'
require 'rbconfig' require 'rbconfig'
@ -336,7 +337,7 @@ module Reline
line_editor.auto_indent_proc = auto_indent_proc line_editor.auto_indent_proc = auto_indent_proc
line_editor.dig_perfect_match_proc = dig_perfect_match_proc line_editor.dig_perfect_match_proc = dig_perfect_match_proc
pre_input_hook&.call pre_input_hook&.call
unless Reline::IOGate == Reline::GeneralIO unless Reline::IOGate.dumb?
@dialog_proc_list.each_pair do |name_sym, d| @dialog_proc_list.each_pair do |name_sym, d|
line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context) line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context)
end end
@ -473,7 +474,7 @@ module Reline
end end
private def may_req_ambiguous_char_width private def may_req_ambiguous_char_width
@ambiguous_width = 2 if io_gate == Reline::GeneralIO or !STDOUT.tty? @ambiguous_width = 2 if io_gate.dumb? or !STDOUT.tty?
return if defined? @ambiguous_width return if defined? @ambiguous_width
io_gate.move_cursor_column(0) io_gate.move_cursor_column(0)
begin begin
@ -573,31 +574,19 @@ module Reline
# Need to change IOGate when `$stdout.tty?` change from false to true by `$stdout.reopen` # Need to change IOGate when `$stdout.tty?` change from false to true by `$stdout.reopen`
# Example: rails/spring boot the application in non-tty, then run console in tty. # Example: rails/spring boot the application in non-tty, then run console in tty.
if ENV['TERM'] != 'dumb' && core.io_gate == Reline::GeneralIO && $stdout.tty? if ENV['TERM'] != 'dumb' && core.io_gate.dumb? && $stdout.tty?
require 'reline/ansi' require 'reline/io/ansi'
remove_const(:IOGate) remove_const(:IOGate)
const_set(:IOGate, Reline::ANSI) const_set(:IOGate, Reline::ANSI.new)
end end
end end
end end
require 'reline/general_io'
io = Reline::GeneralIO Reline::IOGate = Reline::IO.decide_io_gate
unless ENV['TERM'] == 'dumb'
case RbConfig::CONFIG['host_os'] # Deprecated
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ Reline::GeneralIO = Reline::Dumb.new
require 'reline/windows'
tty = (io = Reline::Windows).msys_tty?
else
tty = $stdout.tty?
end
end
Reline::IOGate = if tty
require 'reline/ansi'
Reline::ANSI
else
io
end
Reline::Face.load_initial_configs Reline::Face.load_initial_configs

View File

@ -1,111 +0,0 @@
require 'io/wait'
class Reline::GeneralIO
RESET_COLOR = '' # Do not send color reset sequence
def self.reset(encoding: nil)
@@pasting = false
if encoding
@@encoding = encoding
elsif defined?(@@encoding)
remove_class_variable(:@@encoding)
end
end
def self.encoding
if defined?(@@encoding)
@@encoding
elsif RUBY_PLATFORM =~ /mswin|mingw/
Encoding::UTF_8
else
Encoding::default_external
end
end
def self.win?
false
end
def self.set_default_key_bindings(_)
end
@@buf = []
@@input = STDIN
def self.input=(val)
@@input = val
end
def self.with_raw_input
yield
end
def self.getc(_timeout_second)
unless @@buf.empty?
return @@buf.shift
end
c = nil
loop do
Reline.core.line_editor.handle_signal
result = @@input.wait_readable(0.1)
next if result.nil?
c = @@input.read(1)
break
end
c&.ord
end
def self.ungetc(c)
@@buf.unshift(c)
end
def self.get_screen_size
[24, 80]
end
def self.cursor_pos
Reline::CursorPos.new(1, 1)
end
def self.hide_cursor
end
def self.show_cursor
end
def self.move_cursor_column(val)
end
def self.move_cursor_up(val)
end
def self.move_cursor_down(val)
end
def self.erase_after_cursor
end
def self.scroll_down(val)
end
def self.clear_screen
end
def self.set_screen_size(rows, columns)
end
def self.set_winch_handler(&handler)
end
@@pasting = false
def self.in_pasting?
@@pasting
end
def self.prep
end
def self.deprep(otio)
end
end

45
lib/reline/io.rb Normal file
View File

@ -0,0 +1,45 @@
module Reline
class IO
RESET_COLOR = "\e[0m"
def self.decide_io_gate
if ENV['TERM'] == 'dumb'
Reline::Dumb.new
else
require 'reline/io/ansi'
case RbConfig::CONFIG['host_os']
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
require 'reline/io/windows'
io = Reline::Windows.new
if io.msys_tty?
Reline::ANSI.new
else
io
end
else
if $stdout.tty?
Reline::ANSI.new
else
Reline::Dumb.new
end
end
end
end
def dumb?
false
end
def win?
false
end
def reset_color_sequence
self.class::RESET_COLOR
end
end
end
require 'reline/io/dumb'

View File

@ -1,10 +1,7 @@
require 'io/console' require 'io/console'
require 'io/wait' require 'io/wait'
require_relative 'terminfo'
class Reline::ANSI
RESET_COLOR = "\e[0m"
class Reline::ANSI < Reline::IO
CAPNAME_KEY_BINDINGS = { CAPNAME_KEY_BINDINGS = {
'khome' => :ed_move_to_beg, 'khome' => :ed_move_to_beg,
'kend' => :ed_move_to_end, 'kend' => :ed_move_to_end,
@ -36,15 +33,18 @@ class Reline::ANSI
Reline::Terminfo.setupterm(0, 2) Reline::Terminfo.setupterm(0, 2)
end end
def self.encoding def initialize
@input = STDIN
@output = STDOUT
@buf = []
@old_winch_handler = nil
end
def encoding
Encoding.default_external Encoding.default_external
end end
def self.win? def set_default_key_bindings(config, allow_terminfo: true)
false
end
def self.set_default_key_bindings(config, allow_terminfo: true)
set_bracketed_paste_key_bindings(config) set_bracketed_paste_key_bindings(config)
set_default_key_bindings_ansi_cursor(config) set_default_key_bindings_ansi_cursor(config)
if allow_terminfo && Reline::Terminfo.enabled? if allow_terminfo && Reline::Terminfo.enabled?
@ -67,13 +67,13 @@ class Reline::ANSI
end end
end end
def self.set_bracketed_paste_key_bindings(config) def set_bracketed_paste_key_bindings(config)
[:emacs, :vi_insert, :vi_command].each do |keymap| [:emacs, :vi_insert, :vi_command].each do |keymap|
config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start) config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start)
end end
end end
def self.set_default_key_bindings_ansi_cursor(config) def set_default_key_bindings_ansi_cursor(config)
ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)| ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)|
bindings = [["\e[#{char}", default_func]] # CSI + char bindings = [["\e[#{char}", default_func]] # CSI + char
if modifiers[:ctrl] if modifiers[:ctrl]
@ -95,7 +95,7 @@ class Reline::ANSI
end end
end end
def self.set_default_key_bindings_terminfo(config) def set_default_key_bindings_terminfo(config)
key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding| key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding|
begin begin
key_code = Reline::Terminfo.tigetstr(capname) key_code = Reline::Terminfo.tigetstr(capname)
@ -112,7 +112,7 @@ class Reline::ANSI
end end
end end
def self.set_default_key_bindings_comprehensive_list(config) def set_default_key_bindings_comprehensive_list(config)
{ {
# Console (80x25) # Console (80x25)
[27, 91, 49, 126] => :ed_move_to_beg, # Home [27, 91, 49, 126] => :ed_move_to_beg, # Home
@ -147,37 +147,34 @@ class Reline::ANSI
end end
end end
@@input = STDIN def input=(val)
def self.input=(val) @input = val
@@input = val
end end
@@output = STDOUT def output=(val)
def self.output=(val) @output = val
@@output = val
end end
def self.with_raw_input def with_raw_input
if @@input.tty? if @input.tty?
@@input.raw(intr: true) { yield } @input.raw(intr: true) { yield }
else else
yield yield
end end
end end
@@buf = [] def inner_getc(timeout_second)
def self.inner_getc(timeout_second) unless @buf.empty?
unless @@buf.empty? return @buf.shift
return @@buf.shift
end end
until @@input.wait_readable(0.01) until @input.wait_readable(0.01)
timeout_second -= 0.01 timeout_second -= 0.01
return nil if timeout_second <= 0 return nil if timeout_second <= 0
Reline.core.line_editor.handle_signal Reline.core.line_editor.handle_signal
end end
c = @@input.getbyte c = @input.getbyte
(c == 0x16 && @@input.raw(min: 0, time: 0, &:getbyte)) || c (c == 0x16 && @input.raw(min: 0, time: 0, &:getbyte)) || c
rescue Errno::EIO rescue Errno::EIO
# Maybe the I/O has been closed. # Maybe the I/O has been closed.
nil nil
@ -187,7 +184,7 @@ class Reline::ANSI
START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT) START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT)
END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT) END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT)
def self.read_bracketed_paste def read_bracketed_paste
buffer = String.new(encoding: Encoding::ASCII_8BIT) buffer = String.new(encoding: Encoding::ASCII_8BIT)
until buffer.end_with?(END_BRACKETED_PASTE) until buffer.end_with?(END_BRACKETED_PASTE)
c = inner_getc(Float::INFINITY) c = inner_getc(Float::INFINITY)
@ -199,38 +196,38 @@ class Reline::ANSI
end end
# if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second # if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second
def self.getc(timeout_second) def getc(timeout_second)
inner_getc(timeout_second) inner_getc(timeout_second)
end end
def self.in_pasting? def in_pasting?
not empty_buffer? not empty_buffer?
end end
def self.empty_buffer? def empty_buffer?
unless @@buf.empty? unless @buf.empty?
return false return false
end end
!@@input.wait_readable(0) !@input.wait_readable(0)
end end
def self.ungetc(c) def ungetc(c)
@@buf.unshift(c) @buf.unshift(c)
end end
def self.retrieve_keybuffer def retrieve_keybuffer
begin begin
return unless @@input.wait_readable(0.001) return unless @input.wait_readable(0.001)
str = @@input.read_nonblock(1024) str = @input.read_nonblock(1024)
str.bytes.each do |c| str.bytes.each do |c|
@@buf.push(c) @buf.push(c)
end end
rescue EOFError rescue EOFError
end end
end end
def self.get_screen_size def get_screen_size
s = @@input.winsize s = @input.winsize
return s if s[0] > 0 && s[1] > 0 return s if s[0] > 0 && s[1] > 0
s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i] s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i]
return s if s[0] > 0 && s[1] > 0 return s if s[0] > 0 && s[1] > 0
@ -239,20 +236,20 @@ class Reline::ANSI
[24, 80] [24, 80]
end end
def self.set_screen_size(rows, columns) def set_screen_size(rows, columns)
@@input.winsize = [rows, columns] @input.winsize = [rows, columns]
self self
rescue Errno::ENOTTY rescue Errno::ENOTTY
self self
end end
def self.cursor_pos def cursor_pos
begin begin
res = +'' res = +''
m = nil m = nil
@@input.raw do |stdin| @input.raw do |stdin|
@@output << "\e[6n" @output << "\e[6n"
@@output.flush @output.flush
loop do loop do
c = stdin.getc c = stdin.getc
next if c.nil? next if c.nil?
@ -268,7 +265,7 @@ class Reline::ANSI
row = m[:row].to_i - 1 row = m[:row].to_i - 1
rescue Errno::ENOTTY rescue Errno::ENOTTY
begin begin
buf = @@output.pread(@@output.pos, 0) buf = @output.pread(@output.pos, 0)
row = buf.count("\n") row = buf.count("\n")
column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0 column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0
rescue Errno::ESPIPE, IOError rescue Errno::ESPIPE, IOError
@ -281,30 +278,30 @@ class Reline::ANSI
Reline::CursorPos.new(column, row) Reline::CursorPos.new(column, row)
end end
def self.move_cursor_column(x) def move_cursor_column(x)
@@output.write "\e[#{x + 1}G" @output.write "\e[#{x + 1}G"
end end
def self.move_cursor_up(x) def move_cursor_up(x)
if x > 0 if x > 0
@@output.write "\e[#{x}A" @output.write "\e[#{x}A"
elsif x < 0 elsif x < 0
move_cursor_down(-x) move_cursor_down(-x)
end end
end end
def self.move_cursor_down(x) def move_cursor_down(x)
if x > 0 if x > 0
@@output.write "\e[#{x}B" @output.write "\e[#{x}B"
elsif x < 0 elsif x < 0
move_cursor_up(-x) move_cursor_up(-x)
end end
end end
def self.hide_cursor def hide_cursor
if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
begin begin
@@output.write Reline::Terminfo.tigetstr('civis') @output.write Reline::Terminfo.tigetstr('civis')
rescue Reline::Terminfo::TerminfoError rescue Reline::Terminfo::TerminfoError
# civis is undefined # civis is undefined
end end
@ -313,10 +310,10 @@ class Reline::ANSI
end end
end end
def self.show_cursor def show_cursor
if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
begin begin
@@output.write Reline::Terminfo.tigetstr('cnorm') @output.write Reline::Terminfo.tigetstr('cnorm')
rescue Reline::Terminfo::TerminfoError rescue Reline::Terminfo::TerminfoError
# cnorm is undefined # cnorm is undefined
end end
@ -325,38 +322,37 @@ class Reline::ANSI
end end
end end
def self.erase_after_cursor def erase_after_cursor
@@output.write "\e[K" @output.write "\e[K"
end end
# This only works when the cursor is at the bottom of the scroll range # This only works when the cursor is at the bottom of the scroll range
# For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623 # For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
def self.scroll_down(x) def scroll_down(x)
return if x.zero? return if x.zero?
# We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576 # We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576
@@output.write "\n" * x @output.write "\n" * x
end end
def self.clear_screen def clear_screen
@@output.write "\e[2J" @output.write "\e[2J"
@@output.write "\e[1;1H" @output.write "\e[1;1H"
end end
@@old_winch_handler = nil def set_winch_handler(&handler)
def self.set_winch_handler(&handler) @old_winch_handler = Signal.trap('WINCH', &handler)
@@old_winch_handler = Signal.trap('WINCH', &handler)
end end
def self.prep def prep
# Enable bracketed paste # Enable bracketed paste
@@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste @output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste
retrieve_keybuffer retrieve_keybuffer
nil nil
end end
def self.deprep(otio) def deprep(otio)
# Disable bracketed paste # Disable bracketed paste
@@output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste @output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste
Signal.trap('WINCH', @@old_winch_handler) if @@old_winch_handler Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler
end end
end end

106
lib/reline/io/dumb.rb Normal file
View File

@ -0,0 +1,106 @@
require 'io/wait'
class Reline::Dumb < Reline::IO
RESET_COLOR = '' # Do not send color reset sequence
def initialize(encoding: nil)
@input = STDIN
@buf = []
@pasting = false
@encoding = encoding
@screen_size = [24, 80]
end
def dumb?
true
end
def encoding
if @encoding
@encoding
elsif RUBY_PLATFORM =~ /mswin|mingw/
Encoding::UTF_8
else
Encoding::default_external
end
end
def set_default_key_bindings(_)
end
def input=(val)
@input = val
end
def with_raw_input
yield
end
def getc(_timeout_second)
unless @buf.empty?
return @buf.shift
end
c = nil
loop do
Reline.core.line_editor.handle_signal
result = @input.wait_readable(0.1)
next if result.nil?
c = @input.read(1)
break
end
c&.ord
end
def ungetc(c)
@buf.unshift(c)
end
def get_screen_size
@screen_size
end
def cursor_pos
Reline::CursorPos.new(1, 1)
end
def hide_cursor
end
def show_cursor
end
def move_cursor_column(val)
end
def move_cursor_up(val)
end
def move_cursor_down(val)
end
def erase_after_cursor
end
def scroll_down(val)
end
def clear_screen
end
def set_screen_size(rows, columns)
@screen_size = [rows, columns]
end
def set_winch_handler(&handler)
end
def in_pasting?
@pasting
end
def prep
end
def deprep(otio)
end
end

View File

@ -1,21 +1,49 @@
require 'fiddle/import' require 'fiddle/import'
class Reline::Windows class Reline::Windows < Reline::IO
RESET_COLOR = "\e[0m" def initialize
@input_buf = []
@output_buf = []
def self.encoding @output = STDOUT
@hsg = nil
@getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
@kbhit = Win32API.new('msvcrt', '_kbhit', [], 'I')
@GetKeyState = Win32API.new('user32', 'GetKeyState', ['L'], 'L')
@GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L')
@SetConsoleCursorPosition = Win32API.new('kernel32', 'SetConsoleCursorPosition', ['L', 'L'], 'L')
@GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L')
@FillConsoleOutputCharacter = Win32API.new('kernel32', 'FillConsoleOutputCharacter', ['L', 'L', 'L', 'L', 'P'], 'L')
@ScrollConsoleScreenBuffer = Win32API.new('kernel32', 'ScrollConsoleScreenBuffer', ['L', 'P', 'P', 'L', 'P'], 'L')
@hConsoleHandle = @GetStdHandle.call(STD_OUTPUT_HANDLE)
@hConsoleInputHandle = @GetStdHandle.call(STD_INPUT_HANDLE)
@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
@SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
@WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
@legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0
end
def encoding
Encoding::UTF_8 Encoding::UTF_8
end end
def self.win? def win?
true true
end end
def self.win_legacy_console? def win_legacy_console?
@@legacy_console @legacy_console
end end
def self.set_default_key_bindings(config) def set_default_key_bindings(config)
{ {
[224, 72] => :ed_prev_history, # ↑ [224, 72] => :ed_prev_history, # ↑
[224, 80] => :ed_next_history, # ↓ [224, 80] => :ed_next_history, # ↓
@ -129,58 +157,32 @@ class Reline::Windows
STD_OUTPUT_HANDLE = -11 STD_OUTPUT_HANDLE = -11
FILE_TYPE_PIPE = 0x0003 FILE_TYPE_PIPE = 0x0003
FILE_NAME_INFO = 2 FILE_NAME_INFO = 2
@@getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
@@kbhit = Win32API.new('msvcrt', '_kbhit', [], 'I')
@@GetKeyState = Win32API.new('user32', 'GetKeyState', ['L'], 'L')
@@GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L')
@@SetConsoleCursorPosition = Win32API.new('kernel32', 'SetConsoleCursorPosition', ['L', 'L'], 'L')
@@GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L')
@@FillConsoleOutputCharacter = Win32API.new('kernel32', 'FillConsoleOutputCharacter', ['L', 'L', 'L', 'L', 'P'], 'L')
@@ScrollConsoleScreenBuffer = Win32API.new('kernel32', 'ScrollConsoleScreenBuffer', ['L', 'P', 'P', 'L', 'P'], 'L')
@@hConsoleHandle = @@GetStdHandle.call(STD_OUTPUT_HANDLE)
@@hConsoleInputHandle = @@GetStdHandle.call(STD_INPUT_HANDLE)
@@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
@@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
@@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
@@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
@@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
@@SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
@@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
@@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
@@WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
private_class_method def self.getconsolemode private def getconsolemode
mode = "\000\000\000\000" mode = "\000\000\000\000"
@@GetConsoleMode.call(@@hConsoleHandle, mode) @GetConsoleMode.call(@hConsoleHandle, mode)
mode.unpack1('L') mode.unpack1('L')
end end
private_class_method def self.setconsolemode(mode) private def setconsolemode(mode)
@@SetConsoleMode.call(@@hConsoleHandle, mode) @SetConsoleMode.call(@hConsoleHandle, mode)
end end
@@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) #if @legacy_console
#if @@legacy_console
# setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING) # setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
# @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) # @legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
#end #end
@@input_buf = [] def msys_tty?(io = @hConsoleInputHandle)
@@output_buf = []
@@output = STDOUT
def self.msys_tty?(io = @@hConsoleInputHandle)
# check if fd is a pipe # check if fd is a pipe
if @@GetFileType.call(io) != FILE_TYPE_PIPE if @GetFileType.call(io) != FILE_TYPE_PIPE
return false return false
end end
bufsize = 1024 bufsize = 1024
p_buffer = "\0" * bufsize p_buffer = "\0" * bufsize
res = @@GetFileInformationByHandleEx.call(io, FILE_NAME_INFO, p_buffer, bufsize - 2) res = @GetFileInformationByHandleEx.call(io, FILE_NAME_INFO, p_buffer, bufsize - 2)
return false if res == 0 return false if res == 0
# get pipe name: p_buffer layout is: # get pipe name: p_buffer layout is:
@ -217,65 +219,63 @@ class Reline::Windows
[ { control_keys: :SHIFT, virtual_key_code: VK_TAB }, [27, 91, 90] ], [ { control_keys: :SHIFT, virtual_key_code: VK_TAB }, [27, 91, 90] ],
] ]
@@hsg = nil def process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
# high-surrogate # high-surrogate
if 0xD800 <= char_code and char_code <= 0xDBFF if 0xD800 <= char_code and char_code <= 0xDBFF
@@hsg = char_code @hsg = char_code
return return
end end
# low-surrogate # low-surrogate
if 0xDC00 <= char_code and char_code <= 0xDFFF if 0xDC00 <= char_code and char_code <= 0xDFFF
if @@hsg if @hsg
char_code = 0x10000 + (@@hsg - 0xD800) * 0x400 + char_code - 0xDC00 char_code = 0x10000 + (@hsg - 0xD800) * 0x400 + char_code - 0xDC00
@@hsg = nil @hsg = nil
else else
# no high-surrogate. ignored. # no high-surrogate. ignored.
return return
end end
else else
# ignore high-surrogate without low-surrogate if there # ignore high-surrogate without low-surrogate if there
@@hsg = nil @hsg = nil
end end
key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state) key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
match = KEY_MAP.find { |args,| key.matches?(**args) } match = KEY_MAP.find { |args,| key.matches?(**args) }
unless match.nil? unless match.nil?
@@output_buf.concat(match.last) @output_buf.concat(match.last)
return return
end end
# no char, only control keys # no char, only control keys
return if key.char_code == 0 and key.control_keys.any? return if key.char_code == 0 and key.control_keys.any?
@@output_buf.push("\e".ord) if key.control_keys.include?(:ALT) and !key.control_keys.include?(:CTRL) @output_buf.push("\e".ord) if key.control_keys.include?(:ALT) and !key.control_keys.include?(:CTRL)
@@output_buf.concat(key.char.bytes) @output_buf.concat(key.char.bytes)
end end
def self.check_input_event def check_input_event
num_of_events = 0.chr * 8 num_of_events = 0.chr * 8
while @@output_buf.empty? while @output_buf.empty?
Reline.core.line_editor.handle_signal Reline.core.line_editor.handle_signal
if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec if @WaitForSingleObject.(@hConsoleInputHandle, 100) != 0 # max 0.1 sec
# prevent for background consolemode change # prevent for background consolemode change
@@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) @legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0
next next
end end
next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0 next if @GetNumberOfConsoleInputEvents.(@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0
input_records = 0.chr * 20 * 80 input_records = 0.chr * 20 * 80
read_event = 0.chr * 4 read_event = 0.chr * 4
if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_records, 80, read_event) != 0 if @ReadConsoleInputW.(@hConsoleInputHandle, input_records, 80, read_event) != 0
read_events = read_event.unpack1('L') read_events = read_event.unpack1('L')
0.upto(read_events) do |idx| 0.upto(read_events) do |idx|
input_record = input_records[idx * 20, 20] input_record = input_records[idx * 20, 20]
event = input_record[0, 2].unpack1('s*') event = input_record[0, 2].unpack1('s*')
case event case event
when WINDOW_BUFFER_SIZE_EVENT when WINDOW_BUFFER_SIZE_EVENT
@@winch_handler.() @winch_handler.()
when KEY_EVENT when KEY_EVENT
key_down = input_record[4, 4].unpack1('l*') key_down = input_record[4, 4].unpack1('l*')
repeat_count = input_record[8, 2].unpack1('s*') repeat_count = input_record[8, 2].unpack1('s*')
@ -293,34 +293,34 @@ class Reline::Windows
end end
end end
def self.with_raw_input def with_raw_input
yield yield
end end
def self.getc(_timeout_second) def getc(_timeout_second)
check_input_event check_input_event
@@output_buf.shift @output_buf.shift
end end
def self.ungetc(c) def ungetc(c)
@@output_buf.unshift(c) @output_buf.unshift(c)
end end
def self.in_pasting? def in_pasting?
not self.empty_buffer? not empty_buffer?
end end
def self.empty_buffer? def empty_buffer?
if not @@output_buf.empty? if not @output_buf.empty?
false false
elsif @@kbhit.call == 0 elsif @kbhit.call == 0
true true
else else
false false
end end
end end
def self.get_console_screen_buffer_info def get_console_screen_buffer_info
# CONSOLE_SCREEN_BUFFER_INFO # CONSOLE_SCREEN_BUFFER_INFO
# [ 0,2] dwSize.X # [ 0,2] dwSize.X
# [ 2,2] dwSize.Y # [ 2,2] dwSize.Y
@ -334,18 +334,18 @@ class Reline::Windows
# [18,2] dwMaximumWindowSize.X # [18,2] dwMaximumWindowSize.X
# [20,2] dwMaximumWindowSize.Y # [20,2] dwMaximumWindowSize.Y
csbi = 0.chr * 22 csbi = 0.chr * 22
return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0 return if @GetConsoleScreenBufferInfo.call(@hConsoleHandle, csbi) == 0
csbi csbi
end end
def self.get_screen_size def get_screen_size
unless csbi = get_console_screen_buffer_info unless csbi = get_console_screen_buffer_info
return [1, 1] return [1, 1]
end end
csbi[0, 4].unpack('SS').reverse csbi[0, 4].unpack('SS').reverse
end end
def self.cursor_pos def cursor_pos
unless csbi = get_console_screen_buffer_info unless csbi = get_console_screen_buffer_info
return Reline::CursorPos.new(0, 0) return Reline::CursorPos.new(0, 0)
end end
@ -354,49 +354,49 @@ class Reline::Windows
Reline::CursorPos.new(x, y) Reline::CursorPos.new(x, y)
end end
def self.move_cursor_column(val) def move_cursor_column(val)
@@SetConsoleCursorPosition.call(@@hConsoleHandle, cursor_pos.y * 65536 + val) @SetConsoleCursorPosition.call(@hConsoleHandle, cursor_pos.y * 65536 + val)
end end
def self.move_cursor_up(val) def move_cursor_up(val)
if val > 0 if val > 0
y = cursor_pos.y - val y = cursor_pos.y - val
y = 0 if y < 0 y = 0 if y < 0
@@SetConsoleCursorPosition.call(@@hConsoleHandle, y * 65536 + cursor_pos.x) @SetConsoleCursorPosition.call(@hConsoleHandle, y * 65536 + cursor_pos.x)
elsif val < 0 elsif val < 0
move_cursor_down(-val) move_cursor_down(-val)
end end
end end
def self.move_cursor_down(val) def move_cursor_down(val)
if val > 0 if val > 0
return unless csbi = get_console_screen_buffer_info return unless csbi = get_console_screen_buffer_info
screen_height = get_screen_size.first screen_height = get_screen_size.first
y = cursor_pos.y + val y = cursor_pos.y + val
y = screen_height - 1 if y > (screen_height - 1) y = screen_height - 1 if y > (screen_height - 1)
@@SetConsoleCursorPosition.call(@@hConsoleHandle, (cursor_pos.y + val) * 65536 + cursor_pos.x) @SetConsoleCursorPosition.call(@hConsoleHandle, (cursor_pos.y + val) * 65536 + cursor_pos.x)
elsif val < 0 elsif val < 0
move_cursor_up(-val) move_cursor_up(-val)
end end
end end
def self.erase_after_cursor def erase_after_cursor
return unless csbi = get_console_screen_buffer_info return unless csbi = get_console_screen_buffer_info
attributes = csbi[8, 2].unpack1('S') attributes = csbi[8, 2].unpack1('S')
cursor = csbi[4, 4].unpack1('L') cursor = csbi[4, 4].unpack1('L')
written = 0.chr * 4 written = 0.chr * 4
@@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written) @FillConsoleOutputCharacter.call(@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
@@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, get_screen_size.last - cursor_pos.x, cursor, written) @FillConsoleOutputAttribute.call(@hConsoleHandle, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
end end
def self.scroll_down(val) def scroll_down(val)
return if val < 0 return if val < 0
return unless csbi = get_console_screen_buffer_info return unless csbi = get_console_screen_buffer_info
buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s') buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s')
screen_height = window_bottom - window_top + 1 screen_height = window_bottom - window_top + 1
val = screen_height if val > screen_height val = screen_height if val > screen_height
if @@legacy_console || window_left != 0 if @legacy_console || window_left != 0
# unless ENABLE_VIRTUAL_TERMINAL, # unless ENABLE_VIRTUAL_TERMINAL,
# if srWindow.Left != 0 then it's conhost.exe hosted console # if srWindow.Left != 0 then it's conhost.exe hosted console
# and puts "\n" causes horizontal scroll. its glitch. # and puts "\n" causes horizontal scroll. its glitch.
@ -404,11 +404,11 @@ class Reline::Windows
scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4') scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4')
destination_origin = 0 # y * 65536 + x destination_origin = 0 # y * 65536 + x
fill = [' '.ord, attributes].pack('SS') fill = [' '.ord, attributes].pack('SS')
@@ScrollConsoleScreenBuffer.call(@@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill) @ScrollConsoleScreenBuffer.call(@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill)
else else
origin_x = x + 1 origin_x = x + 1
origin_y = y - window_top + 1 origin_y = y - window_top + 1
@@output.write [ @output.write [
(origin_y != screen_height) ? "\e[#{screen_height};H" : nil, (origin_y != screen_height) ? "\e[#{screen_height};H" : nil,
"\n" * val, "\n" * val,
(origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil (origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil
@ -416,49 +416,49 @@ class Reline::Windows
end end
end end
def self.clear_screen def clear_screen
if @@legacy_console if @legacy_console
return unless csbi = get_console_screen_buffer_info return unless csbi = get_console_screen_buffer_info
buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s') buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s')
fill_length = buffer_width * (window_bottom - window_top + 1) fill_length = buffer_width * (window_bottom - window_top + 1)
screen_topleft = window_top * 65536 screen_topleft = window_top * 65536
written = 0.chr * 4 written = 0.chr * 4
@@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written) @FillConsoleOutputCharacter.call(@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
@@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written) @FillConsoleOutputAttribute.call(@hConsoleHandle, attributes, fill_length, screen_topleft, written)
@@SetConsoleCursorPosition.call(@@hConsoleHandle, screen_topleft) @SetConsoleCursorPosition.call(@hConsoleHandle, screen_topleft)
else else
@@output.write "\e[2J" "\e[H" @output.write "\e[2J" "\e[H"
end end
end end
def self.set_screen_size(rows, columns) def set_screen_size(rows, columns)
raise NotImplementedError raise NotImplementedError
end end
def self.hide_cursor def hide_cursor
size = 100 size = 100
visible = 0 # 0 means false visible = 0 # 0 means false
cursor_info = [size, visible].pack('Li') cursor_info = [size, visible].pack('Li')
@@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info) @SetConsoleCursorInfo.call(@hConsoleHandle, cursor_info)
end end
def self.show_cursor def show_cursor
size = 100 size = 100
visible = 1 # 1 means true visible = 1 # 1 means true
cursor_info = [size, visible].pack('Li') cursor_info = [size, visible].pack('Li')
@@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info) @SetConsoleCursorInfo.call(@hConsoleHandle, cursor_info)
end end
def self.set_winch_handler(&handler) def set_winch_handler(&handler)
@@winch_handler = handler @winch_handler = handler
end end
def self.prep def prep
# do nothing # do nothing
nil nil
end end
def self.deprep(otio) def deprep(otio)
# do nothing # do nothing
end end

View File

@ -412,7 +412,7 @@ class Reline::LineEditor
# do nothing # do nothing
elsif level == :blank elsif level == :blank
Reline::IOGate.move_cursor_column base_x Reline::IOGate.move_cursor_column base_x
@output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}" @output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}"
else else
x, w, content = new_items[level] x, w, content = new_items[level]
cover_begin = base_x != 0 && new_levels[base_x - 1] == level cover_begin = base_x != 0 && new_levels[base_x - 1] == level
@ -422,7 +422,7 @@ class Reline::LineEditor
content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true) content, pos = Reline::Unicode.take_mbchar_range(content, base_x - x, width, cover_begin: cover_begin, cover_end: cover_end, padding: true)
end end
Reline::IOGate.move_cursor_column x + pos Reline::IOGate.move_cursor_column x + pos
@output.write "#{Reline::IOGate::RESET_COLOR}#{content}#{Reline::IOGate::RESET_COLOR}" @output.write "#{Reline::IOGate.reset_color_sequence}#{content}#{Reline::IOGate.reset_color_sequence}"
end end
base_x += width base_x += width
end end

View File

@ -22,29 +22,36 @@ module Reline
class <<self class <<self
def test_mode(ansi: false) def test_mode(ansi: false)
@original_iogate = IOGate @original_iogate = IOGate
remove_const('IOGate')
const_set('IOGate', ansi ? Reline::ANSI : Reline::GeneralIO)
if ENV['RELINE_TEST_ENCODING'] if ENV['RELINE_TEST_ENCODING']
encoding = Encoding.find(ENV['RELINE_TEST_ENCODING']) encoding = Encoding.find(ENV['RELINE_TEST_ENCODING'])
else else
encoding = Encoding::UTF_8 encoding = Encoding::UTF_8
end end
@original_get_screen_size = IOGate.method(:get_screen_size)
IOGate.singleton_class.remove_method(:get_screen_size) if ansi
def IOGate.get_screen_size new_io_gate = ANSI.new
[24, 80] # Setting ANSI gate's screen size through set_screen_size will also change the tester's stdin's screen size
# Let's avoid that side-effect by stubbing the get_screen_size method
new_io_gate.define_singleton_method(:get_screen_size) do
[24, 80]
end
new_io_gate.define_singleton_method(:encoding) do
encoding
end
else
new_io_gate = Dumb.new(encoding: encoding)
end end
Reline::GeneralIO.reset(encoding: encoding) unless ansi
remove_const('IOGate')
const_set('IOGate', new_io_gate)
core.config.instance_variable_set(:@test_mode, true) core.config.instance_variable_set(:@test_mode, true)
core.config.reset core.config.reset
end end
def test_reset def test_reset
IOGate.singleton_class.remove_method(:get_screen_size)
IOGate.define_singleton_method(:get_screen_size, @original_get_screen_size)
remove_const('IOGate') remove_const('IOGate')
const_set('IOGate', @original_iogate) const_set('IOGate', @original_iogate)
Reline::GeneralIO.reset
Reline.instance_variable_set(:@core, nil) Reline.instance_variable_set(:@core, nil)
end end
@ -146,7 +153,7 @@ class Reline::TestCase < Test::Unit::TestCase
expected.bytesize, byte_pointer, expected.bytesize, byte_pointer,
<<~EOM) <<~EOM)
<#{expected.inspect} (#{expected.encoding.inspect})> expected but was <#{expected.inspect} (#{expected.encoding.inspect})> expected but was
<#{chunk.inspect} (#{chunk.encoding.inspect})> in <Terminal #{Reline::GeneralIO.encoding.inspect}> <#{chunk.inspect} (#{chunk.encoding.inspect})> in <Terminal #{Reline::Dumb.new.encoding.inspect}>
EOM EOM
end end

View File

@ -1,7 +1,7 @@
require_relative 'helper' require_relative 'helper'
require 'reline/ansi' require 'reline'
class Reline::ANSI::TestWithTerminfo < Reline::TestCase class Reline::ANSI::WithTerminfoTest < Reline::TestCase
def setup def setup
Reline.send(:test_mode, ansi: true) Reline.send(:test_mode, ansi: true)
@config = Reline::Config.new @config = Reline::Config.new

View File

@ -1,7 +1,7 @@
require_relative 'helper' require_relative 'helper'
require 'reline/ansi' require 'reline'
class Reline::ANSI::TestWithoutTerminfo < Reline::TestCase class Reline::ANSI::WithoutTerminfoTest < Reline::TestCase
def setup def setup
Reline.send(:test_mode, ansi: true) Reline.send(:test_mode, ansi: true)
@config = Reline::Config.new @config = Reline::Config.new

View File

@ -85,15 +85,13 @@ class Reline::Config::Test < Reline::TestCase
def test_encoding_is_ascii def test_encoding_is_ascii
@config.reset @config.reset
Reline.core.io_gate.reset(encoding: Encoding::US_ASCII) Reline.core.io_gate.instance_variable_set(:@encoding, Encoding::US_ASCII)
@config = Reline::Config.new @config = Reline::Config.new
assert_equal true, @config.convert_meta assert_equal true, @config.convert_meta
end end
def test_encoding_is_not_ascii def test_encoding_is_not_ascii
@config.reset
Reline.core.io_gate.reset(encoding: Encoding::UTF_8)
@config = Reline::Config.new @config = Reline::Config.new
assert_equal nil, @config.convert_meta assert_equal nil, @config.convert_meta

View File

@ -4,14 +4,12 @@ require 'stringio'
class Reline::LineEditor class Reline::LineEditor
class RenderLineDifferentialTest < Reline::TestCase class RenderLineDifferentialTest < Reline::TestCase
module TestIO class TestIO < Reline::IO
RESET_COLOR = "\e[0m" def move_cursor_column(col)
def self.move_cursor_column(col)
@output << "[COL_#{col}]" @output << "[COL_#{col}]"
end end
def self.erase_after_cursor def erase_after_cursor
@output << '[ERASE]' @output << '[ERASE]'
end end
end end
@ -24,7 +22,7 @@ class Reline::LineEditor
@line_editor.instance_variable_set(:@screen_size, [24, 80]) @line_editor.instance_variable_set(:@screen_size, [24, 80])
@line_editor.instance_variable_set(:@output, @output) @line_editor.instance_variable_set(:@output, @output)
Reline.send(:remove_const, :IOGate) Reline.send(:remove_const, :IOGate)
Reline.const_set(:IOGate, TestIO) Reline.const_set(:IOGate, TestIO.new)
Reline::IOGate.instance_variable_set(:@output, @output) Reline::IOGate.instance_variable_set(:@output, @output)
ensure ensure
$VERBOSE = verbose $VERBOSE = verbose

View File

@ -375,7 +375,7 @@ class Reline::Test < Reline::TestCase
def test_dumb_terminal def test_dumb_terminal
lib = File.expand_path("../../lib", __dir__) lib = File.expand_path("../../lib", __dir__)
out = IO.popen([{"TERM"=>"dumb"}, Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", "p Reline.core.io_gate"], &:read) out = IO.popen([{"TERM"=>"dumb"}, Reline.test_rubybin, "-I#{lib}", "-rreline", "-e", "p Reline.core.io_gate"], &:read)
assert_equal("Reline::GeneralIO", out.chomp) assert_match(/#<Reline::Dumb/, out.chomp)
end end
def test_require_reline_should_not_trigger_winsize def test_require_reline_should_not_trigger_winsize
@ -389,7 +389,7 @@ class Reline::Test < Reline::TestCase
require("reline") && p(Reline.core.io_gate) require("reline") && p(Reline.core.io_gate)
RUBY RUBY
out = IO.popen([{}, Reline.test_rubybin, "-I#{lib}", "-e", code], &:read) out = IO.popen([{}, Reline.test_rubybin, "-I#{lib}", "-e", code], &:read)
assert_equal("Reline::ANSI", out.chomp) assert_include(out.chomp, "Reline::ANSI")
end end
def win? def win?