[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/history'
require 'reline/terminfo'
require 'reline/io'
require 'reline/face'
require 'rbconfig'
@ -336,7 +337,7 @@ module Reline
line_editor.auto_indent_proc = auto_indent_proc
line_editor.dig_perfect_match_proc = dig_perfect_match_proc
pre_input_hook&.call
unless Reline::IOGate == Reline::GeneralIO
unless Reline::IOGate.dumb?
@dialog_proc_list.each_pair do |name_sym, d|
line_editor.add_dialog_proc(name_sym, d.dialog_proc, d.context)
end
@ -473,7 +474,7 @@ module Reline
end
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
io_gate.move_cursor_column(0)
begin
@ -573,31 +574,19 @@ module Reline
# 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.
if ENV['TERM'] != 'dumb' && core.io_gate == Reline::GeneralIO && $stdout.tty?
require 'reline/ansi'
if ENV['TERM'] != 'dumb' && core.io_gate.dumb? && $stdout.tty?
require 'reline/io/ansi'
remove_const(:IOGate)
const_set(:IOGate, Reline::ANSI)
const_set(:IOGate, Reline::ANSI.new)
end
end
end
require 'reline/general_io'
io = Reline::GeneralIO
unless ENV['TERM'] == 'dumb'
case RbConfig::CONFIG['host_os']
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
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::IOGate = Reline::IO.decide_io_gate
# Deprecated
Reline::GeneralIO = Reline::Dumb.new
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/wait'
require_relative 'terminfo'
class Reline::ANSI
RESET_COLOR = "\e[0m"
class Reline::ANSI < Reline::IO
CAPNAME_KEY_BINDINGS = {
'khome' => :ed_move_to_beg,
'kend' => :ed_move_to_end,
@ -36,15 +33,18 @@ class Reline::ANSI
Reline::Terminfo.setupterm(0, 2)
end
def self.encoding
def initialize
@input = STDIN
@output = STDOUT
@buf = []
@old_winch_handler = nil
end
def encoding
Encoding.default_external
end
def self.win?
false
end
def self.set_default_key_bindings(config, allow_terminfo: true)
def set_default_key_bindings(config, allow_terminfo: true)
set_bracketed_paste_key_bindings(config)
set_default_key_bindings_ansi_cursor(config)
if allow_terminfo && Reline::Terminfo.enabled?
@ -67,13 +67,13 @@ class Reline::ANSI
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|
config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start)
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)|
bindings = [["\e[#{char}", default_func]] # CSI + char
if modifiers[:ctrl]
@ -95,7 +95,7 @@ class Reline::ANSI
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|
begin
key_code = Reline::Terminfo.tigetstr(capname)
@ -112,7 +112,7 @@ class Reline::ANSI
end
end
def self.set_default_key_bindings_comprehensive_list(config)
def set_default_key_bindings_comprehensive_list(config)
{
# Console (80x25)
[27, 91, 49, 126] => :ed_move_to_beg, # Home
@ -147,37 +147,34 @@ class Reline::ANSI
end
end
@@input = STDIN
def self.input=(val)
@@input = val
def input=(val)
@input = val
end
@@output = STDOUT
def self.output=(val)
@@output = val
def output=(val)
@output = val
end
def self.with_raw_input
if @@input.tty?
@@input.raw(intr: true) { yield }
def with_raw_input
if @input.tty?
@input.raw(intr: true) { yield }
else
yield
end
end
@@buf = []
def self.inner_getc(timeout_second)
unless @@buf.empty?
return @@buf.shift
def inner_getc(timeout_second)
unless @buf.empty?
return @buf.shift
end
until @@input.wait_readable(0.01)
until @input.wait_readable(0.01)
timeout_second -= 0.01
return nil if timeout_second <= 0
Reline.core.line_editor.handle_signal
end
c = @@input.getbyte
(c == 0x16 && @@input.raw(min: 0, time: 0, &:getbyte)) || c
c = @input.getbyte
(c == 0x16 && @input.raw(min: 0, time: 0, &:getbyte)) || c
rescue Errno::EIO
# Maybe the I/O has been closed.
nil
@ -187,7 +184,7 @@ class Reline::ANSI
START_BRACKETED_PASTE = String.new("\e[200~", 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)
until buffer.end_with?(END_BRACKETED_PASTE)
c = inner_getc(Float::INFINITY)
@ -199,38 +196,38 @@ class Reline::ANSI
end
# 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)
end
def self.in_pasting?
def in_pasting?
not empty_buffer?
end
def self.empty_buffer?
unless @@buf.empty?
def empty_buffer?
unless @buf.empty?
return false
end
!@@input.wait_readable(0)
!@input.wait_readable(0)
end
def self.ungetc(c)
@@buf.unshift(c)
def ungetc(c)
@buf.unshift(c)
end
def self.retrieve_keybuffer
def retrieve_keybuffer
begin
return unless @@input.wait_readable(0.001)
str = @@input.read_nonblock(1024)
return unless @input.wait_readable(0.001)
str = @input.read_nonblock(1024)
str.bytes.each do |c|
@@buf.push(c)
@buf.push(c)
end
rescue EOFError
end
end
def self.get_screen_size
s = @@input.winsize
def get_screen_size
s = @input.winsize
return s if s[0] > 0 && s[1] > 0
s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i]
return s if s[0] > 0 && s[1] > 0
@ -239,20 +236,20 @@ class Reline::ANSI
[24, 80]
end
def self.set_screen_size(rows, columns)
@@input.winsize = [rows, columns]
def set_screen_size(rows, columns)
@input.winsize = [rows, columns]
self
rescue Errno::ENOTTY
self
end
def self.cursor_pos
def cursor_pos
begin
res = +''
m = nil
@@input.raw do |stdin|
@@output << "\e[6n"
@@output.flush
@input.raw do |stdin|
@output << "\e[6n"
@output.flush
loop do
c = stdin.getc
next if c.nil?
@ -268,7 +265,7 @@ class Reline::ANSI
row = m[:row].to_i - 1
rescue Errno::ENOTTY
begin
buf = @@output.pread(@@output.pos, 0)
buf = @output.pread(@output.pos, 0)
row = buf.count("\n")
column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0
rescue Errno::ESPIPE, IOError
@ -281,30 +278,30 @@ class Reline::ANSI
Reline::CursorPos.new(column, row)
end
def self.move_cursor_column(x)
@@output.write "\e[#{x + 1}G"
def move_cursor_column(x)
@output.write "\e[#{x + 1}G"
end
def self.move_cursor_up(x)
def move_cursor_up(x)
if x > 0
@@output.write "\e[#{x}A"
@output.write "\e[#{x}A"
elsif x < 0
move_cursor_down(-x)
end
end
def self.move_cursor_down(x)
def move_cursor_down(x)
if x > 0
@@output.write "\e[#{x}B"
@output.write "\e[#{x}B"
elsif x < 0
move_cursor_up(-x)
end
end
def self.hide_cursor
def hide_cursor
if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
begin
@@output.write Reline::Terminfo.tigetstr('civis')
@output.write Reline::Terminfo.tigetstr('civis')
rescue Reline::Terminfo::TerminfoError
# civis is undefined
end
@ -313,10 +310,10 @@ class Reline::ANSI
end
end
def self.show_cursor
def show_cursor
if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
begin
@@output.write Reline::Terminfo.tigetstr('cnorm')
@output.write Reline::Terminfo.tigetstr('cnorm')
rescue Reline::Terminfo::TerminfoError
# cnorm is undefined
end
@ -325,38 +322,37 @@ class Reline::ANSI
end
end
def self.erase_after_cursor
@@output.write "\e[K"
def erase_after_cursor
@output.write "\e[K"
end
# 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
def self.scroll_down(x)
def scroll_down(x)
return if x.zero?
# 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
def self.clear_screen
@@output.write "\e[2J"
@@output.write "\e[1;1H"
def clear_screen
@output.write "\e[2J"
@output.write "\e[1;1H"
end
@@old_winch_handler = nil
def self.set_winch_handler(&handler)
@@old_winch_handler = Signal.trap('WINCH', &handler)
def set_winch_handler(&handler)
@old_winch_handler = Signal.trap('WINCH', &handler)
end
def self.prep
def prep
# 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
nil
end
def self.deprep(otio)
def deprep(otio)
# Disable bracketed paste
@@output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste
Signal.trap('WINCH', @@old_winch_handler) if @@old_winch_handler
@output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste
Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler
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'
class Reline::Windows
RESET_COLOR = "\e[0m"
class Reline::Windows < Reline::IO
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
end
def self.win?
def win?
true
end
def self.win_legacy_console?
@@legacy_console
def win_legacy_console?
@legacy_console
end
def self.set_default_key_bindings(config)
def set_default_key_bindings(config)
{
[224, 72] => :ed_prev_history, # ↑
[224, 80] => :ed_next_history, # ↓
@ -129,58 +157,32 @@ class Reline::Windows
STD_OUTPUT_HANDLE = -11
FILE_TYPE_PIPE = 0x0003
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
private_class_method def self.getconsolemode
private def getconsolemode
mode = "\000\000\000\000"
@@GetConsoleMode.call(@@hConsoleHandle, mode)
@GetConsoleMode.call(@hConsoleHandle, mode)
mode.unpack1('L')
end
private_class_method def self.setconsolemode(mode)
@@SetConsoleMode.call(@@hConsoleHandle, mode)
private def setconsolemode(mode)
@SetConsoleMode.call(@hConsoleHandle, mode)
end
@@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
#if @@legacy_console
#if @legacy_console
# setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
# @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
# @legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
#end
@@input_buf = []
@@output_buf = []
@@output = STDOUT
def self.msys_tty?(io = @@hConsoleInputHandle)
def msys_tty?(io = @hConsoleInputHandle)
# check if fd is a pipe
if @@GetFileType.call(io) != FILE_TYPE_PIPE
if @GetFileType.call(io) != FILE_TYPE_PIPE
return false
end
bufsize = 1024
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
# 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] ],
]
@@hsg = nil
def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
def process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
# high-surrogate
if 0xD800 <= char_code and char_code <= 0xDBFF
@@hsg = char_code
@hsg = char_code
return
end
# low-surrogate
if 0xDC00 <= char_code and char_code <= 0xDFFF
if @@hsg
char_code = 0x10000 + (@@hsg - 0xD800) * 0x400 + char_code - 0xDC00
@@hsg = nil
if @hsg
char_code = 0x10000 + (@hsg - 0xD800) * 0x400 + char_code - 0xDC00
@hsg = nil
else
# no high-surrogate. ignored.
return
end
else
# ignore high-surrogate without low-surrogate if there
@@hsg = nil
@hsg = nil
end
key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
match = KEY_MAP.find { |args,| key.matches?(**args) }
unless match.nil?
@@output_buf.concat(match.last)
@output_buf.concat(match.last)
return
end
# no char, only control keys
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
def self.check_input_event
def check_input_event
num_of_events = 0.chr * 8
while @@output_buf.empty?
while @output_buf.empty?
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
@@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
@legacy_console = getconsolemode & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0
next
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
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')
0.upto(read_events) do |idx|
input_record = input_records[idx * 20, 20]
event = input_record[0, 2].unpack1('s*')
case event
when WINDOW_BUFFER_SIZE_EVENT
@@winch_handler.()
@winch_handler.()
when KEY_EVENT
key_down = input_record[4, 4].unpack1('l*')
repeat_count = input_record[8, 2].unpack1('s*')
@ -293,34 +293,34 @@ class Reline::Windows
end
end
def self.with_raw_input
def with_raw_input
yield
end
def self.getc(_timeout_second)
def getc(_timeout_second)
check_input_event
@@output_buf.shift
@output_buf.shift
end
def self.ungetc(c)
@@output_buf.unshift(c)
def ungetc(c)
@output_buf.unshift(c)
end
def self.in_pasting?
not self.empty_buffer?
def in_pasting?
not empty_buffer?
end
def self.empty_buffer?
if not @@output_buf.empty?
def empty_buffer?
if not @output_buf.empty?
false
elsif @@kbhit.call == 0
elsif @kbhit.call == 0
true
else
false
end
end
def self.get_console_screen_buffer_info
def get_console_screen_buffer_info
# CONSOLE_SCREEN_BUFFER_INFO
# [ 0,2] dwSize.X
# [ 2,2] dwSize.Y
@ -334,18 +334,18 @@ class Reline::Windows
# [18,2] dwMaximumWindowSize.X
# [20,2] dwMaximumWindowSize.Y
csbi = 0.chr * 22
return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0
return if @GetConsoleScreenBufferInfo.call(@hConsoleHandle, csbi) == 0
csbi
end
def self.get_screen_size
def get_screen_size
unless csbi = get_console_screen_buffer_info
return [1, 1]
end
csbi[0, 4].unpack('SS').reverse
end
def self.cursor_pos
def cursor_pos
unless csbi = get_console_screen_buffer_info
return Reline::CursorPos.new(0, 0)
end
@ -354,49 +354,49 @@ class Reline::Windows
Reline::CursorPos.new(x, y)
end
def self.move_cursor_column(val)
@@SetConsoleCursorPosition.call(@@hConsoleHandle, cursor_pos.y * 65536 + val)
def move_cursor_column(val)
@SetConsoleCursorPosition.call(@hConsoleHandle, cursor_pos.y * 65536 + val)
end
def self.move_cursor_up(val)
def move_cursor_up(val)
if val > 0
y = cursor_pos.y - val
y = 0 if y < 0
@@SetConsoleCursorPosition.call(@@hConsoleHandle, y * 65536 + cursor_pos.x)
@SetConsoleCursorPosition.call(@hConsoleHandle, y * 65536 + cursor_pos.x)
elsif val < 0
move_cursor_down(-val)
end
end
def self.move_cursor_down(val)
def move_cursor_down(val)
if val > 0
return unless csbi = get_console_screen_buffer_info
screen_height = get_screen_size.first
y = cursor_pos.y + val
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
move_cursor_up(-val)
end
end
def self.erase_after_cursor
def erase_after_cursor
return unless csbi = get_console_screen_buffer_info
attributes = csbi[8, 2].unpack1('S')
cursor = csbi[4, 4].unpack1('L')
written = 0.chr * 4
@@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)
@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)
end
def self.scroll_down(val)
def scroll_down(val)
return if val < 0
return unless csbi = get_console_screen_buffer_info
buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s')
screen_height = window_bottom - window_top + 1
val = screen_height if val > screen_height
if @@legacy_console || window_left != 0
if @legacy_console || window_left != 0
# unless ENABLE_VIRTUAL_TERMINAL,
# if srWindow.Left != 0 then it's conhost.exe hosted console
# 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')
destination_origin = 0 # y * 65536 + x
fill = [' '.ord, attributes].pack('SS')
@@ScrollConsoleScreenBuffer.call(@@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill)
@ScrollConsoleScreenBuffer.call(@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill)
else
origin_x = x + 1
origin_y = y - window_top + 1
@@output.write [
@output.write [
(origin_y != screen_height) ? "\e[#{screen_height};H" : nil,
"\n" * val,
(origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil
@ -416,49 +416,49 @@ class Reline::Windows
end
end
def self.clear_screen
if @@legacy_console
def clear_screen
if @legacy_console
return unless csbi = get_console_screen_buffer_info
buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s')
fill_length = buffer_width * (window_bottom - window_top + 1)
screen_topleft = window_top * 65536
written = 0.chr * 4
@@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
@@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written)
@@SetConsoleCursorPosition.call(@@hConsoleHandle, screen_topleft)
@FillConsoleOutputCharacter.call(@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
@FillConsoleOutputAttribute.call(@hConsoleHandle, attributes, fill_length, screen_topleft, written)
@SetConsoleCursorPosition.call(@hConsoleHandle, screen_topleft)
else
@@output.write "\e[2J" "\e[H"
@output.write "\e[2J" "\e[H"
end
end
def self.set_screen_size(rows, columns)
def set_screen_size(rows, columns)
raise NotImplementedError
end
def self.hide_cursor
def hide_cursor
size = 100
visible = 0 # 0 means false
cursor_info = [size, visible].pack('Li')
@@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
@SetConsoleCursorInfo.call(@hConsoleHandle, cursor_info)
end
def self.show_cursor
def show_cursor
size = 100
visible = 1 # 1 means true
cursor_info = [size, visible].pack('Li')
@@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
@SetConsoleCursorInfo.call(@hConsoleHandle, cursor_info)
end
def self.set_winch_handler(&handler)
@@winch_handler = handler
def set_winch_handler(&handler)
@winch_handler = handler
end
def self.prep
def prep
# do nothing
nil
end
def self.deprep(otio)
def deprep(otio)
# do nothing
end

View File

@ -412,7 +412,7 @@ class Reline::LineEditor
# do nothing
elsif level == :blank
Reline::IOGate.move_cursor_column base_x
@output.write "#{Reline::IOGate::RESET_COLOR}#{' ' * width}"
@output.write "#{Reline::IOGate.reset_color_sequence}#{' ' * width}"
else
x, w, content = new_items[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)
end
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
base_x += width
end

View File

@ -22,29 +22,36 @@ module Reline
class <<self
def test_mode(ansi: false)
@original_iogate = IOGate
remove_const('IOGate')
const_set('IOGate', ansi ? Reline::ANSI : Reline::GeneralIO)
if ENV['RELINE_TEST_ENCODING']
encoding = Encoding.find(ENV['RELINE_TEST_ENCODING'])
else
encoding = Encoding::UTF_8
end
@original_get_screen_size = IOGate.method(:get_screen_size)
IOGate.singleton_class.remove_method(:get_screen_size)
def IOGate.get_screen_size
[24, 80]
if ansi
new_io_gate = ANSI.new
# 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
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.reset
end
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')
const_set('IOGate', @original_iogate)
Reline::GeneralIO.reset
Reline.instance_variable_set(:@core, nil)
end
@ -146,7 +153,7 @@ class Reline::TestCase < Test::Unit::TestCase
expected.bytesize, byte_pointer,
<<~EOM)
<#{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
end

View File

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

View File

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

View File

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

View File

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

View File

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