From cda69b5910494a745d87b7932547341cb2fefe3a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sat, 1 Jun 2024 11:28:03 +0100 Subject: [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 --------- https://github.com/ruby/reline/commit/dc1518e1ac Co-authored-by: tomoya ishida --- lib/reline.rb | 33 +- lib/reline/ansi.rb | 362 --------------------- lib/reline/general_io.rb | 111 ------- lib/reline/io.rb | 45 +++ lib/reline/io/ansi.rb | 358 +++++++++++++++++++++ lib/reline/io/dumb.rb | 106 +++++++ lib/reline/io/windows.rb | 503 ++++++++++++++++++++++++++++++ lib/reline/line_editor.rb | 4 +- lib/reline/windows.rb | 503 ------------------------------ test/reline/helper.rb | 29 +- test/reline/test_ansi_with_terminfo.rb | 4 +- test/reline/test_ansi_without_terminfo.rb | 4 +- test/reline/test_config.rb | 4 +- test/reline/test_line_editor.rb | 10 +- test/reline/test_reline.rb | 4 +- 15 files changed, 1054 insertions(+), 1026 deletions(-) delete mode 100644 lib/reline/ansi.rb delete mode 100644 lib/reline/general_io.rb create mode 100644 lib/reline/io.rb create mode 100644 lib/reline/io/ansi.rb create mode 100644 lib/reline/io/dumb.rb create mode 100644 lib/reline/io/windows.rb delete mode 100644 lib/reline/windows.rb diff --git a/lib/reline.rb b/lib/reline.rb index fb00b96531..796e637e85 100644 --- a/lib/reline.rb +++ b/lib/reline.rb @@ -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 diff --git a/lib/reline/ansi.rb b/lib/reline/ansi.rb deleted file mode 100644 index fa9feb2630..0000000000 --- a/lib/reline/ansi.rb +++ /dev/null @@ -1,362 +0,0 @@ -require 'io/console' -require 'io/wait' -require_relative 'terminfo' - -class Reline::ANSI - RESET_COLOR = "\e[0m" - - CAPNAME_KEY_BINDINGS = { - 'khome' => :ed_move_to_beg, - 'kend' => :ed_move_to_end, - 'kdch1' => :key_delete, - 'kpp' => :ed_search_prev_history, - 'knp' => :ed_search_next_history, - 'kcuu1' => :ed_prev_history, - 'kcud1' => :ed_next_history, - 'kcuf1' => :ed_next_char, - 'kcub1' => :ed_prev_char, - } - - ANSI_CURSOR_KEY_BINDINGS = { - # Up - 'A' => [:ed_prev_history, {}], - # Down - 'B' => [:ed_next_history, {}], - # Right - 'C' => [:ed_next_char, { ctrl: :em_next_word, meta: :em_next_word }], - # Left - 'D' => [:ed_prev_char, { ctrl: :ed_prev_word, meta: :ed_prev_word }], - # End - 'F' => [:ed_move_to_end, {}], - # Home - 'H' => [:ed_move_to_beg, {}], - } - - if Reline::Terminfo.enabled? - Reline::Terminfo.setupterm(0, 2) - end - - def self.encoding - Encoding.default_external - end - - def self.win? - false - end - - def self.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? - set_default_key_bindings_terminfo(config) - else - set_default_key_bindings_comprehensive_list(config) - end - { - [27, 91, 90] => :completion_journey_up, # S-Tab - }.each_pair do |key, func| - config.add_default_key_binding_by_keymap(:emacs, key, func) - config.add_default_key_binding_by_keymap(:vi_insert, key, func) - end - { - # default bindings - [27, 32] => :em_set_mark, # M- - [24, 24] => :em_exchange_mark, # C-x C-x - }.each_pair do |key, func| - config.add_default_key_binding_by_keymap(:emacs, key, func) - end - end - - def self.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) - ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)| - bindings = [["\e[#{char}", default_func]] # CSI + char - if modifiers[:ctrl] - # CSI + ctrl_key_modifier + char - bindings << ["\e[1;5#{char}", modifiers[:ctrl]] - end - if modifiers[:meta] - # CSI + meta_key_modifier + char - bindings << ["\e[1;3#{char}", modifiers[:meta]] - # Meta(ESC) + CSI + char - bindings << ["\e\e[#{char}", modifiers[:meta]] - end - bindings.each do |sequence, func| - key = sequence.bytes - config.add_default_key_binding_by_keymap(:emacs, key, func) - config.add_default_key_binding_by_keymap(:vi_insert, key, func) - config.add_default_key_binding_by_keymap(:vi_command, key, func) - end - end - end - - def self.set_default_key_bindings_terminfo(config) - key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding| - begin - key_code = Reline::Terminfo.tigetstr(capname) - [ key_code.bytes, key_binding ] - rescue Reline::Terminfo::TerminfoError - # capname is undefined - end - end.compact.to_h - - key_bindings.each_pair do |key, func| - config.add_default_key_binding_by_keymap(:emacs, key, func) - config.add_default_key_binding_by_keymap(:vi_insert, key, func) - config.add_default_key_binding_by_keymap(:vi_command, key, func) - end - end - - def self.set_default_key_bindings_comprehensive_list(config) - { - # Console (80x25) - [27, 91, 49, 126] => :ed_move_to_beg, # Home - [27, 91, 52, 126] => :ed_move_to_end, # End - [27, 91, 51, 126] => :key_delete, # Del - - # KDE - # Del is 0x08 - [27, 71, 65] => :ed_prev_history, # ↑ - [27, 71, 66] => :ed_next_history, # ↓ - [27, 71, 67] => :ed_next_char, # → - [27, 71, 68] => :ed_prev_char, # ← - - # urxvt / exoterm - [27, 91, 55, 126] => :ed_move_to_beg, # Home - [27, 91, 56, 126] => :ed_move_to_end, # End - - # GNOME - [27, 79, 72] => :ed_move_to_beg, # Home - [27, 79, 70] => :ed_move_to_end, # End - # Del is 0x08 - # Arrow keys are the same of KDE - - [27, 79, 65] => :ed_prev_history, # ↑ - [27, 79, 66] => :ed_next_history, # ↓ - [27, 79, 67] => :ed_next_char, # → - [27, 79, 68] => :ed_prev_char, # ← - }.each_pair do |key, func| - config.add_default_key_binding_by_keymap(:emacs, key, func) - config.add_default_key_binding_by_keymap(:vi_insert, key, func) - config.add_default_key_binding_by_keymap(:vi_command, key, func) - end - end - - @@input = STDIN - def self.input=(val) - @@input = val - end - - @@output = STDOUT - def self.output=(val) - @@output = val - end - - def self.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 - end - 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 - rescue Errno::EIO - # Maybe the I/O has been closed. - nil - rescue Errno::ENOTTY - nil - end - - 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 - buffer = String.new(encoding: Encoding::ASCII_8BIT) - until buffer.end_with?(END_BRACKETED_PASTE) - c = inner_getc(Float::INFINITY) - break unless c - buffer << c - end - string = buffer.delete_suffix(END_BRACKETED_PASTE).force_encoding(encoding) - string.valid_encoding? ? string : '' - end - - # if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second - def self.getc(timeout_second) - inner_getc(timeout_second) - end - - def self.in_pasting? - not empty_buffer? - end - - def self.empty_buffer? - unless @@buf.empty? - return false - end - !@@input.wait_readable(0) - end - - def self.ungetc(c) - @@buf.unshift(c) - end - - def self.retrieve_keybuffer - begin - return unless @@input.wait_readable(0.001) - str = @@input.read_nonblock(1024) - str.bytes.each do |c| - @@buf.push(c) - end - rescue EOFError - end - end - - def self.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 - [24, 80] - rescue Errno::ENOTTY, Errno::ENODEV - [24, 80] - end - - def self.set_screen_size(rows, columns) - @@input.winsize = [rows, columns] - self - rescue Errno::ENOTTY - self - end - - def self.cursor_pos - begin - res = +'' - m = nil - @@input.raw do |stdin| - @@output << "\e[6n" - @@output.flush - loop do - c = stdin.getc - next if c.nil? - res << c - m = res.match(/\e\[(?\d+);(?\d+)R/) - break if m - end - (m.pre_match + m.post_match).chars.reverse_each do |ch| - stdin.ungetc ch - end - end - column = m[:column].to_i - 1 - row = m[:row].to_i - 1 - rescue Errno::ENOTTY - begin - 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 - # Just returns column 1 for ambiguous width because this I/O is not - # tty and can't seek. - row = 0 - column = 1 - end - end - Reline::CursorPos.new(column, row) - end - - def self.move_cursor_column(x) - @@output.write "\e[#{x + 1}G" - end - - def self.move_cursor_up(x) - if x > 0 - @@output.write "\e[#{x}A" - elsif x < 0 - move_cursor_down(-x) - end - end - - def self.move_cursor_down(x) - if x > 0 - @@output.write "\e[#{x}B" - elsif x < 0 - move_cursor_up(-x) - end - end - - def self.hide_cursor - if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? - begin - @@output.write Reline::Terminfo.tigetstr('civis') - rescue Reline::Terminfo::TerminfoError - # civis is undefined - end - else - # ignored - end - end - - def self.show_cursor - if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? - begin - @@output.write Reline::Terminfo.tigetstr('cnorm') - rescue Reline::Terminfo::TerminfoError - # cnorm is undefined - end - else - # ignored - end - end - - def self.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) - 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 - end - - def self.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) - end - - def self.prep - # Enable bracketed paste - @@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste - retrieve_keybuffer - nil - end - - def self.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 - end -end diff --git a/lib/reline/general_io.rb b/lib/reline/general_io.rb deleted file mode 100644 index d52151ad3c..0000000000 --- a/lib/reline/general_io.rb +++ /dev/null @@ -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 diff --git a/lib/reline/io.rb b/lib/reline/io.rb new file mode 100644 index 0000000000..7fca0c338a --- /dev/null +++ b/lib/reline/io.rb @@ -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' diff --git a/lib/reline/io/ansi.rb b/lib/reline/io/ansi.rb new file mode 100644 index 0000000000..cf3c9965dd --- /dev/null +++ b/lib/reline/io/ansi.rb @@ -0,0 +1,358 @@ +require 'io/console' +require 'io/wait' + +class Reline::ANSI < Reline::IO + CAPNAME_KEY_BINDINGS = { + 'khome' => :ed_move_to_beg, + 'kend' => :ed_move_to_end, + 'kdch1' => :key_delete, + 'kpp' => :ed_search_prev_history, + 'knp' => :ed_search_next_history, + 'kcuu1' => :ed_prev_history, + 'kcud1' => :ed_next_history, + 'kcuf1' => :ed_next_char, + 'kcub1' => :ed_prev_char, + } + + ANSI_CURSOR_KEY_BINDINGS = { + # Up + 'A' => [:ed_prev_history, {}], + # Down + 'B' => [:ed_next_history, {}], + # Right + 'C' => [:ed_next_char, { ctrl: :em_next_word, meta: :em_next_word }], + # Left + 'D' => [:ed_prev_char, { ctrl: :ed_prev_word, meta: :ed_prev_word }], + # End + 'F' => [:ed_move_to_end, {}], + # Home + 'H' => [:ed_move_to_beg, {}], + } + + if Reline::Terminfo.enabled? + Reline::Terminfo.setupterm(0, 2) + end + + def initialize + @input = STDIN + @output = STDOUT + @buf = [] + @old_winch_handler = nil + end + + def encoding + Encoding.default_external + end + + 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? + set_default_key_bindings_terminfo(config) + else + set_default_key_bindings_comprehensive_list(config) + end + { + [27, 91, 90] => :completion_journey_up, # S-Tab + }.each_pair do |key, func| + config.add_default_key_binding_by_keymap(:emacs, key, func) + config.add_default_key_binding_by_keymap(:vi_insert, key, func) + end + { + # default bindings + [27, 32] => :em_set_mark, # M- + [24, 24] => :em_exchange_mark, # C-x C-x + }.each_pair do |key, func| + config.add_default_key_binding_by_keymap(:emacs, key, func) + end + end + + 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 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] + # CSI + ctrl_key_modifier + char + bindings << ["\e[1;5#{char}", modifiers[:ctrl]] + end + if modifiers[:meta] + # CSI + meta_key_modifier + char + bindings << ["\e[1;3#{char}", modifiers[:meta]] + # Meta(ESC) + CSI + char + bindings << ["\e\e[#{char}", modifiers[:meta]] + end + bindings.each do |sequence, func| + key = sequence.bytes + config.add_default_key_binding_by_keymap(:emacs, key, func) + config.add_default_key_binding_by_keymap(:vi_insert, key, func) + config.add_default_key_binding_by_keymap(:vi_command, key, func) + end + end + end + + def set_default_key_bindings_terminfo(config) + key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding| + begin + key_code = Reline::Terminfo.tigetstr(capname) + [ key_code.bytes, key_binding ] + rescue Reline::Terminfo::TerminfoError + # capname is undefined + end + end.compact.to_h + + key_bindings.each_pair do |key, func| + config.add_default_key_binding_by_keymap(:emacs, key, func) + config.add_default_key_binding_by_keymap(:vi_insert, key, func) + config.add_default_key_binding_by_keymap(:vi_command, key, func) + end + end + + def set_default_key_bindings_comprehensive_list(config) + { + # Console (80x25) + [27, 91, 49, 126] => :ed_move_to_beg, # Home + [27, 91, 52, 126] => :ed_move_to_end, # End + [27, 91, 51, 126] => :key_delete, # Del + + # KDE + # Del is 0x08 + [27, 71, 65] => :ed_prev_history, # ↑ + [27, 71, 66] => :ed_next_history, # ↓ + [27, 71, 67] => :ed_next_char, # → + [27, 71, 68] => :ed_prev_char, # ← + + # urxvt / exoterm + [27, 91, 55, 126] => :ed_move_to_beg, # Home + [27, 91, 56, 126] => :ed_move_to_end, # End + + # GNOME + [27, 79, 72] => :ed_move_to_beg, # Home + [27, 79, 70] => :ed_move_to_end, # End + # Del is 0x08 + # Arrow keys are the same of KDE + + [27, 79, 65] => :ed_prev_history, # ↑ + [27, 79, 66] => :ed_next_history, # ↓ + [27, 79, 67] => :ed_next_char, # → + [27, 79, 68] => :ed_prev_char, # ← + }.each_pair do |key, func| + config.add_default_key_binding_by_keymap(:emacs, key, func) + config.add_default_key_binding_by_keymap(:vi_insert, key, func) + config.add_default_key_binding_by_keymap(:vi_command, key, func) + end + end + + def input=(val) + @input = val + end + + def output=(val) + @output = val + end + + def with_raw_input + if @input.tty? + @input.raw(intr: true) { yield } + else + yield + end + end + + def inner_getc(timeout_second) + unless @buf.empty? + return @buf.shift + end + 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 + rescue Errno::EIO + # Maybe the I/O has been closed. + nil + rescue Errno::ENOTTY + nil + end + + START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT) + END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT) + def read_bracketed_paste + buffer = String.new(encoding: Encoding::ASCII_8BIT) + until buffer.end_with?(END_BRACKETED_PASTE) + c = inner_getc(Float::INFINITY) + break unless c + buffer << c + end + string = buffer.delete_suffix(END_BRACKETED_PASTE).force_encoding(encoding) + string.valid_encoding? ? string : '' + end + + # if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second + def getc(timeout_second) + inner_getc(timeout_second) + end + + def in_pasting? + not empty_buffer? + end + + def empty_buffer? + unless @buf.empty? + return false + end + !@input.wait_readable(0) + end + + def ungetc(c) + @buf.unshift(c) + end + + def retrieve_keybuffer + begin + return unless @input.wait_readable(0.001) + str = @input.read_nonblock(1024) + str.bytes.each do |c| + @buf.push(c) + end + rescue EOFError + end + end + + 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 + [24, 80] + rescue Errno::ENOTTY, Errno::ENODEV + [24, 80] + end + + def set_screen_size(rows, columns) + @input.winsize = [rows, columns] + self + rescue Errno::ENOTTY + self + end + + def cursor_pos + begin + res = +'' + m = nil + @input.raw do |stdin| + @output << "\e[6n" + @output.flush + loop do + c = stdin.getc + next if c.nil? + res << c + m = res.match(/\e\[(?\d+);(?\d+)R/) + break if m + end + (m.pre_match + m.post_match).chars.reverse_each do |ch| + stdin.ungetc ch + end + end + column = m[:column].to_i - 1 + row = m[:row].to_i - 1 + rescue Errno::ENOTTY + begin + 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 + # Just returns column 1 for ambiguous width because this I/O is not + # tty and can't seek. + row = 0 + column = 1 + end + end + Reline::CursorPos.new(column, row) + end + + def move_cursor_column(x) + @output.write "\e[#{x + 1}G" + end + + def move_cursor_up(x) + if x > 0 + @output.write "\e[#{x}A" + elsif x < 0 + move_cursor_down(-x) + end + end + + def move_cursor_down(x) + if x > 0 + @output.write "\e[#{x}B" + elsif x < 0 + move_cursor_up(-x) + end + end + + def hide_cursor + if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? + begin + @output.write Reline::Terminfo.tigetstr('civis') + rescue Reline::Terminfo::TerminfoError + # civis is undefined + end + else + # ignored + end + end + + def show_cursor + if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported? + begin + @output.write Reline::Terminfo.tigetstr('cnorm') + rescue Reline::Terminfo::TerminfoError + # cnorm is undefined + end + else + # ignored + end + end + + 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 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 + end + + def clear_screen + @output.write "\e[2J" + @output.write "\e[1;1H" + end + + def set_winch_handler(&handler) + @old_winch_handler = Signal.trap('WINCH', &handler) + end + + def prep + # Enable bracketed paste + @output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste + retrieve_keybuffer + nil + end + + 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 + end +end diff --git a/lib/reline/io/dumb.rb b/lib/reline/io/dumb.rb new file mode 100644 index 0000000000..6ed69ffdfa --- /dev/null +++ b/lib/reline/io/dumb.rb @@ -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 diff --git a/lib/reline/io/windows.rb b/lib/reline/io/windows.rb new file mode 100644 index 0000000000..6ba4b830d6 --- /dev/null +++ b/lib/reline/io/windows.rb @@ -0,0 +1,503 @@ +require 'fiddle/import' + +class Reline::Windows < Reline::IO + def initialize + @input_buf = [] + @output_buf = [] + + @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 win? + true + end + + def win_legacy_console? + @legacy_console + end + + def set_default_key_bindings(config) + { + [224, 72] => :ed_prev_history, # ↑ + [224, 80] => :ed_next_history, # ↓ + [224, 77] => :ed_next_char, # → + [224, 75] => :ed_prev_char, # ← + [224, 83] => :key_delete, # Del + [224, 71] => :ed_move_to_beg, # Home + [224, 79] => :ed_move_to_end, # End + [ 0, 41] => :ed_unassigned, # input method on/off + [ 0, 72] => :ed_prev_history, # ↑ + [ 0, 80] => :ed_next_history, # ↓ + [ 0, 77] => :ed_next_char, # → + [ 0, 75] => :ed_prev_char, # ← + [ 0, 83] => :key_delete, # Del + [ 0, 71] => :ed_move_to_beg, # Home + [ 0, 79] => :ed_move_to_end # End + }.each_pair do |key, func| + config.add_default_key_binding_by_keymap(:emacs, key, func) + config.add_default_key_binding_by_keymap(:vi_insert, key, func) + config.add_default_key_binding_by_keymap(:vi_command, key, func) + end + + { + [27, 32] => :em_set_mark, # M- + [24, 24] => :em_exchange_mark, # C-x C-x + }.each_pair do |key, func| + config.add_default_key_binding_by_keymap(:emacs, key, func) + end + + # Emulate ANSI key sequence. + { + [27, 91, 90] => :completion_journey_up, # S-Tab + }.each_pair do |key, func| + config.add_default_key_binding_by_keymap(:emacs, key, func) + config.add_default_key_binding_by_keymap(:vi_insert, key, func) + end + end + + if defined? JRUBY_VERSION + require 'win32api' + else + class Win32API + DLL = {} + TYPEMAP = {"0" => Fiddle::TYPE_VOID, "S" => Fiddle::TYPE_VOIDP, "I" => Fiddle::TYPE_LONG} + POINTER_TYPE = Fiddle::SIZEOF_VOIDP == Fiddle::SIZEOF_LONG_LONG ? 'q*' : 'l!*' + + WIN32_TYPES = "VPpNnLlIi" + DL_TYPES = "0SSI" + + def initialize(dllname, func, import, export = "0", calltype = :stdcall) + @proto = [import].join.tr(WIN32_TYPES, DL_TYPES).sub(/^(.)0*$/, '\1') + import = @proto.chars.map {|win_type| TYPEMAP[win_type.tr(WIN32_TYPES, DL_TYPES)]} + export = TYPEMAP[export.tr(WIN32_TYPES, DL_TYPES)] + calltype = Fiddle::Importer.const_get(:CALL_TYPE_TO_ABI)[calltype] + + handle = DLL[dllname] ||= + begin + Fiddle.dlopen(dllname) + rescue Fiddle::DLError + raise unless File.extname(dllname).empty? + Fiddle.dlopen(dllname + ".dll") + end + + @func = Fiddle::Function.new(handle[func], import, export, calltype) + rescue Fiddle::DLError => e + raise LoadError, e.message, e.backtrace + end + + def call(*args) + import = @proto.split("") + args.each_with_index do |x, i| + args[i], = [x == 0 ? nil : +x].pack("p").unpack(POINTER_TYPE) if import[i] == "S" + args[i], = [x].pack("I").unpack("i") if import[i] == "I" + end + ret, = @func.call(*args) + return ret || 0 + end + end + end + + VK_RETURN = 0x0D + VK_MENU = 0x12 # ALT key + VK_LMENU = 0xA4 + VK_CONTROL = 0x11 + VK_SHIFT = 0x10 + VK_DIVIDE = 0x6F + + KEY_EVENT = 0x01 + WINDOW_BUFFER_SIZE_EVENT = 0x04 + + CAPSLOCK_ON = 0x0080 + ENHANCED_KEY = 0x0100 + LEFT_ALT_PRESSED = 0x0002 + LEFT_CTRL_PRESSED = 0x0008 + NUMLOCK_ON = 0x0020 + RIGHT_ALT_PRESSED = 0x0001 + RIGHT_CTRL_PRESSED = 0x0004 + SCROLLLOCK_ON = 0x0040 + SHIFT_PRESSED = 0x0010 + + VK_TAB = 0x09 + VK_END = 0x23 + VK_HOME = 0x24 + VK_LEFT = 0x25 + VK_UP = 0x26 + VK_RIGHT = 0x27 + VK_DOWN = 0x28 + VK_DELETE = 0x2E + + STD_INPUT_HANDLE = -10 + STD_OUTPUT_HANDLE = -11 + FILE_TYPE_PIPE = 0x0003 + FILE_NAME_INFO = 2 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + + private def getconsolemode + mode = "\000\000\000\000" + @GetConsoleMode.call(@hConsoleHandle, mode) + mode.unpack1('L') + end + + private def setconsolemode(mode) + @SetConsoleMode.call(@hConsoleHandle, mode) + end + + #if @legacy_console + # setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + # @legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) + #end + + def msys_tty?(io = @hConsoleInputHandle) + # check if fd is a 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) + return false if res == 0 + + # get pipe name: p_buffer layout is: + # struct _FILE_NAME_INFO { + # DWORD FileNameLength; + # WCHAR FileName[1]; + # } FILE_NAME_INFO + len = p_buffer[0, 4].unpack1("L") + name = p_buffer[4, len].encode(Encoding::UTF_8, Encoding::UTF_16LE, invalid: :replace) + + # Check if this could be a MSYS2 pty pipe ('\msys-XXXX-ptyN-XX') + # or a cygwin pty pipe ('\cygwin-XXXX-ptyN-XX') + name =~ /(msys-|cygwin-).*-pty/ ? true : false + end + + KEY_MAP = [ + # It's treated as Meta+Enter on Windows. + [ { control_keys: :CTRL, virtual_key_code: 0x0D }, "\e\r".bytes ], + [ { control_keys: :SHIFT, virtual_key_code: 0x0D }, "\e\r".bytes ], + + # It's treated as Meta+Space on Windows. + [ { control_keys: :CTRL, char_code: 0x20 }, "\e ".bytes ], + + # Emulate getwch() key sequences. + [ { control_keys: [], virtual_key_code: VK_UP }, [0, 72] ], + [ { control_keys: [], virtual_key_code: VK_DOWN }, [0, 80] ], + [ { control_keys: [], virtual_key_code: VK_RIGHT }, [0, 77] ], + [ { control_keys: [], virtual_key_code: VK_LEFT }, [0, 75] ], + [ { control_keys: [], virtual_key_code: VK_DELETE }, [0, 83] ], + [ { control_keys: [], virtual_key_code: VK_HOME }, [0, 71] ], + [ { control_keys: [], virtual_key_code: VK_END }, [0, 79] ], + + # Emulate ANSI key sequence. + [ { control_keys: :SHIFT, virtual_key_code: VK_TAB }, [27, 91, 90] ], + ] + + 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 + 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 + else + # no high-surrogate. ignored. + return + end + else + # ignore high-surrogate without low-surrogate if there + @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) + 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.concat(key.char.bytes) + end + + def check_input_event + num_of_events = 0.chr * 8 + while @output_buf.empty? + Reline.core.line_editor.handle_signal + if @WaitForSingleObject.(@hConsoleInputHandle, 100) != 0 # max 0.1 sec + # prevent for background consolemode change + @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 + input_records = 0.chr * 20 * 80 + read_event = 0.chr * 4 + 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.() + when KEY_EVENT + key_down = input_record[4, 4].unpack1('l*') + repeat_count = input_record[8, 2].unpack1('s*') + virtual_key_code = input_record[10, 2].unpack1('s*') + virtual_scan_code = input_record[12, 2].unpack1('s*') + char_code = input_record[14, 2].unpack1('S*') + control_key_state = input_record[16, 2].unpack1('S*') + is_key_down = key_down.zero? ? false : true + if is_key_down + process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state) + end + end + end + end + end + end + + def with_raw_input + yield + end + + def getc(_timeout_second) + check_input_event + @output_buf.shift + end + + def ungetc(c) + @output_buf.unshift(c) + end + + def in_pasting? + not empty_buffer? + end + + def empty_buffer? + if not @output_buf.empty? + false + elsif @kbhit.call == 0 + true + else + false + end + end + + def get_console_screen_buffer_info + # CONSOLE_SCREEN_BUFFER_INFO + # [ 0,2] dwSize.X + # [ 2,2] dwSize.Y + # [ 4,2] dwCursorPositions.X + # [ 6,2] dwCursorPositions.Y + # [ 8,2] wAttributes + # [10,2] srWindow.Left + # [12,2] srWindow.Top + # [14,2] srWindow.Right + # [16,2] srWindow.Bottom + # [18,2] dwMaximumWindowSize.X + # [20,2] dwMaximumWindowSize.Y + csbi = 0.chr * 22 + return if @GetConsoleScreenBufferInfo.call(@hConsoleHandle, csbi) == 0 + csbi + end + + def get_screen_size + unless csbi = get_console_screen_buffer_info + return [1, 1] + end + csbi[0, 4].unpack('SS').reverse + end + + def cursor_pos + unless csbi = get_console_screen_buffer_info + return Reline::CursorPos.new(0, 0) + end + x = csbi[4, 2].unpack1('s') + y = csbi[6, 2].unpack1('s') + Reline::CursorPos.new(x, y) + end + + def move_cursor_column(val) + @SetConsoleCursorPosition.call(@hConsoleHandle, cursor_pos.y * 65536 + val) + end + + 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) + elsif val < 0 + move_cursor_down(-val) + end + end + + 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) + elsif val < 0 + move_cursor_up(-val) + end + end + + 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) + end + + 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 + # unless ENABLE_VIRTUAL_TERMINAL, + # if srWindow.Left != 0 then it's conhost.exe hosted console + # and puts "\n" causes horizontal scroll. its glitch. + # FYI irb write from culumn 1, so this gives no gain. + 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) + else + origin_x = x + 1 + origin_y = y - window_top + 1 + @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 + ].join + end + end + + 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) + else + @output.write "\e[2J" "\e[H" + end + end + + def set_screen_size(rows, columns) + raise NotImplementedError + end + + def hide_cursor + size = 100 + visible = 0 # 0 means false + cursor_info = [size, visible].pack('Li') + @SetConsoleCursorInfo.call(@hConsoleHandle, cursor_info) + end + + def show_cursor + size = 100 + visible = 1 # 1 means true + cursor_info = [size, visible].pack('Li') + @SetConsoleCursorInfo.call(@hConsoleHandle, cursor_info) + end + + def set_winch_handler(&handler) + @winch_handler = handler + end + + def prep + # do nothing + nil + end + + def deprep(otio) + # do nothing + end + + class KeyEventRecord + + attr_reader :virtual_key_code, :char_code, :control_key_state, :control_keys + + def initialize(virtual_key_code, char_code, control_key_state) + @virtual_key_code = virtual_key_code + @char_code = char_code + @control_key_state = control_key_state + @enhanced = control_key_state & ENHANCED_KEY != 0 + + (@control_keys = []).tap do |control_keys| + # symbols must be sorted to make comparison is easier later on + control_keys << :ALT if control_key_state & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0 + control_keys << :CTRL if control_key_state & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0 + control_keys << :SHIFT if control_key_state & SHIFT_PRESSED != 0 + end.freeze + end + + def char + @char_code.chr(Encoding::UTF_8) + end + + def enhanced? + @enhanced + end + + # Verifies if the arguments match with this key event. + # Nil arguments are ignored, but at least one must be passed as non-nil. + # To verify that no control keys were pressed, pass an empty array: `control_keys: []`. + def matches?(control_keys: nil, virtual_key_code: nil, char_code: nil) + raise ArgumentError, 'No argument was passed to match key event' if control_keys.nil? && virtual_key_code.nil? && char_code.nil? + + (control_keys.nil? || [*control_keys].sort == @control_keys) && + (virtual_key_code.nil? || @virtual_key_code == virtual_key_code) && + (char_code.nil? || char_code == @char_code) + end + + end +end diff --git a/lib/reline/line_editor.rb b/lib/reline/line_editor.rb index 8f6421fb10..c2f5f0622e 100644 --- a/lib/reline/line_editor.rb +++ b/lib/reline/line_editor.rb @@ -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 diff --git a/lib/reline/windows.rb b/lib/reline/windows.rb deleted file mode 100644 index ee3f73e383..0000000000 --- a/lib/reline/windows.rb +++ /dev/null @@ -1,503 +0,0 @@ -require 'fiddle/import' - -class Reline::Windows - RESET_COLOR = "\e[0m" - - def self.encoding - Encoding::UTF_8 - end - - def self.win? - true - end - - def self.win_legacy_console? - @@legacy_console - end - - def self.set_default_key_bindings(config) - { - [224, 72] => :ed_prev_history, # ↑ - [224, 80] => :ed_next_history, # ↓ - [224, 77] => :ed_next_char, # → - [224, 75] => :ed_prev_char, # ← - [224, 83] => :key_delete, # Del - [224, 71] => :ed_move_to_beg, # Home - [224, 79] => :ed_move_to_end, # End - [ 0, 41] => :ed_unassigned, # input method on/off - [ 0, 72] => :ed_prev_history, # ↑ - [ 0, 80] => :ed_next_history, # ↓ - [ 0, 77] => :ed_next_char, # → - [ 0, 75] => :ed_prev_char, # ← - [ 0, 83] => :key_delete, # Del - [ 0, 71] => :ed_move_to_beg, # Home - [ 0, 79] => :ed_move_to_end # End - }.each_pair do |key, func| - config.add_default_key_binding_by_keymap(:emacs, key, func) - config.add_default_key_binding_by_keymap(:vi_insert, key, func) - config.add_default_key_binding_by_keymap(:vi_command, key, func) - end - - { - [27, 32] => :em_set_mark, # M- - [24, 24] => :em_exchange_mark, # C-x C-x - }.each_pair do |key, func| - config.add_default_key_binding_by_keymap(:emacs, key, func) - end - - # Emulate ANSI key sequence. - { - [27, 91, 90] => :completion_journey_up, # S-Tab - }.each_pair do |key, func| - config.add_default_key_binding_by_keymap(:emacs, key, func) - config.add_default_key_binding_by_keymap(:vi_insert, key, func) - end - end - - if defined? JRUBY_VERSION - require 'win32api' - else - class Win32API - DLL = {} - TYPEMAP = {"0" => Fiddle::TYPE_VOID, "S" => Fiddle::TYPE_VOIDP, "I" => Fiddle::TYPE_LONG} - POINTER_TYPE = Fiddle::SIZEOF_VOIDP == Fiddle::SIZEOF_LONG_LONG ? 'q*' : 'l!*' - - WIN32_TYPES = "VPpNnLlIi" - DL_TYPES = "0SSI" - - def initialize(dllname, func, import, export = "0", calltype = :stdcall) - @proto = [import].join.tr(WIN32_TYPES, DL_TYPES).sub(/^(.)0*$/, '\1') - import = @proto.chars.map {|win_type| TYPEMAP[win_type.tr(WIN32_TYPES, DL_TYPES)]} - export = TYPEMAP[export.tr(WIN32_TYPES, DL_TYPES)] - calltype = Fiddle::Importer.const_get(:CALL_TYPE_TO_ABI)[calltype] - - handle = DLL[dllname] ||= - begin - Fiddle.dlopen(dllname) - rescue Fiddle::DLError - raise unless File.extname(dllname).empty? - Fiddle.dlopen(dllname + ".dll") - end - - @func = Fiddle::Function.new(handle[func], import, export, calltype) - rescue Fiddle::DLError => e - raise LoadError, e.message, e.backtrace - end - - def call(*args) - import = @proto.split("") - args.each_with_index do |x, i| - args[i], = [x == 0 ? nil : +x].pack("p").unpack(POINTER_TYPE) if import[i] == "S" - args[i], = [x].pack("I").unpack("i") if import[i] == "I" - end - ret, = @func.call(*args) - return ret || 0 - end - end - end - - VK_RETURN = 0x0D - VK_MENU = 0x12 # ALT key - VK_LMENU = 0xA4 - VK_CONTROL = 0x11 - VK_SHIFT = 0x10 - VK_DIVIDE = 0x6F - - KEY_EVENT = 0x01 - WINDOW_BUFFER_SIZE_EVENT = 0x04 - - CAPSLOCK_ON = 0x0080 - ENHANCED_KEY = 0x0100 - LEFT_ALT_PRESSED = 0x0002 - LEFT_CTRL_PRESSED = 0x0008 - NUMLOCK_ON = 0x0020 - RIGHT_ALT_PRESSED = 0x0001 - RIGHT_CTRL_PRESSED = 0x0004 - SCROLLLOCK_ON = 0x0040 - SHIFT_PRESSED = 0x0010 - - VK_TAB = 0x09 - VK_END = 0x23 - VK_HOME = 0x24 - VK_LEFT = 0x25 - VK_UP = 0x26 - VK_RIGHT = 0x27 - VK_DOWN = 0x28 - VK_DELETE = 0x2E - - STD_INPUT_HANDLE = -10 - 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 - mode = "\000\000\000\000" - @@GetConsoleMode.call(@@hConsoleHandle, mode) - mode.unpack1('L') - end - - private_class_method def self.setconsolemode(mode) - @@SetConsoleMode.call(@@hConsoleHandle, mode) - end - - @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) - #if @@legacy_console - # setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING) - # @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0) - #end - - @@input_buf = [] - @@output_buf = [] - - @@output = STDOUT - - def self.msys_tty?(io = @@hConsoleInputHandle) - # check if fd is a 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) - return false if res == 0 - - # get pipe name: p_buffer layout is: - # struct _FILE_NAME_INFO { - # DWORD FileNameLength; - # WCHAR FileName[1]; - # } FILE_NAME_INFO - len = p_buffer[0, 4].unpack1("L") - name = p_buffer[4, len].encode(Encoding::UTF_8, Encoding::UTF_16LE, invalid: :replace) - - # Check if this could be a MSYS2 pty pipe ('\msys-XXXX-ptyN-XX') - # or a cygwin pty pipe ('\cygwin-XXXX-ptyN-XX') - name =~ /(msys-|cygwin-).*-pty/ ? true : false - end - - KEY_MAP = [ - # It's treated as Meta+Enter on Windows. - [ { control_keys: :CTRL, virtual_key_code: 0x0D }, "\e\r".bytes ], - [ { control_keys: :SHIFT, virtual_key_code: 0x0D }, "\e\r".bytes ], - - # It's treated as Meta+Space on Windows. - [ { control_keys: :CTRL, char_code: 0x20 }, "\e ".bytes ], - - # Emulate getwch() key sequences. - [ { control_keys: [], virtual_key_code: VK_UP }, [0, 72] ], - [ { control_keys: [], virtual_key_code: VK_DOWN }, [0, 80] ], - [ { control_keys: [], virtual_key_code: VK_RIGHT }, [0, 77] ], - [ { control_keys: [], virtual_key_code: VK_LEFT }, [0, 75] ], - [ { control_keys: [], virtual_key_code: VK_DELETE }, [0, 83] ], - [ { control_keys: [], virtual_key_code: VK_HOME }, [0, 71] ], - [ { control_keys: [], virtual_key_code: VK_END }, [0, 79] ], - - # Emulate ANSI key sequence. - [ { 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) - - # high-surrogate - if 0xD800 <= char_code and char_code <= 0xDBFF - @@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 - else - # no high-surrogate. ignored. - return - end - else - # ignore high-surrogate without low-surrogate if there - @@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) - 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.concat(key.char.bytes) - end - - def self.check_input_event - num_of_events = 0.chr * 8 - while @@output_buf.empty? - Reline.core.line_editor.handle_signal - if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec - # prevent for background consolemode change - @@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 - input_records = 0.chr * 20 * 80 - read_event = 0.chr * 4 - 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.() - when KEY_EVENT - key_down = input_record[4, 4].unpack1('l*') - repeat_count = input_record[8, 2].unpack1('s*') - virtual_key_code = input_record[10, 2].unpack1('s*') - virtual_scan_code = input_record[12, 2].unpack1('s*') - char_code = input_record[14, 2].unpack1('S*') - control_key_state = input_record[16, 2].unpack1('S*') - is_key_down = key_down.zero? ? false : true - if is_key_down - process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state) - end - end - end - end - end - end - - def self.with_raw_input - yield - end - - def self.getc(_timeout_second) - check_input_event - @@output_buf.shift - end - - def self.ungetc(c) - @@output_buf.unshift(c) - end - - def self.in_pasting? - not self.empty_buffer? - end - - def self.empty_buffer? - if not @@output_buf.empty? - false - elsif @@kbhit.call == 0 - true - else - false - end - end - - def self.get_console_screen_buffer_info - # CONSOLE_SCREEN_BUFFER_INFO - # [ 0,2] dwSize.X - # [ 2,2] dwSize.Y - # [ 4,2] dwCursorPositions.X - # [ 6,2] dwCursorPositions.Y - # [ 8,2] wAttributes - # [10,2] srWindow.Left - # [12,2] srWindow.Top - # [14,2] srWindow.Right - # [16,2] srWindow.Bottom - # [18,2] dwMaximumWindowSize.X - # [20,2] dwMaximumWindowSize.Y - csbi = 0.chr * 22 - return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0 - csbi - end - - def self.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 - unless csbi = get_console_screen_buffer_info - return Reline::CursorPos.new(0, 0) - end - x = csbi[4, 2].unpack1('s') - y = csbi[6, 2].unpack1('s') - Reline::CursorPos.new(x, y) - end - - def self.move_cursor_column(val) - @@SetConsoleCursorPosition.call(@@hConsoleHandle, cursor_pos.y * 65536 + val) - end - - def self.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) - elsif val < 0 - move_cursor_down(-val) - end - end - - def self.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) - elsif val < 0 - move_cursor_up(-val) - end - end - - def self.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) - end - - def self.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 - # unless ENABLE_VIRTUAL_TERMINAL, - # if srWindow.Left != 0 then it's conhost.exe hosted console - # and puts "\n" causes horizontal scroll. its glitch. - # FYI irb write from culumn 1, so this gives no gain. - 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) - else - origin_x = x + 1 - origin_y = y - window_top + 1 - @@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 - ].join - end - end - - def self.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) - else - @@output.write "\e[2J" "\e[H" - end - end - - def self.set_screen_size(rows, columns) - raise NotImplementedError - end - - def self.hide_cursor - size = 100 - visible = 0 # 0 means false - cursor_info = [size, visible].pack('Li') - @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info) - end - - def self.show_cursor - size = 100 - visible = 1 # 1 means true - cursor_info = [size, visible].pack('Li') - @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info) - end - - def self.set_winch_handler(&handler) - @@winch_handler = handler - end - - def self.prep - # do nothing - nil - end - - def self.deprep(otio) - # do nothing - end - - class KeyEventRecord - - attr_reader :virtual_key_code, :char_code, :control_key_state, :control_keys - - def initialize(virtual_key_code, char_code, control_key_state) - @virtual_key_code = virtual_key_code - @char_code = char_code - @control_key_state = control_key_state - @enhanced = control_key_state & ENHANCED_KEY != 0 - - (@control_keys = []).tap do |control_keys| - # symbols must be sorted to make comparison is easier later on - control_keys << :ALT if control_key_state & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0 - control_keys << :CTRL if control_key_state & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0 - control_keys << :SHIFT if control_key_state & SHIFT_PRESSED != 0 - end.freeze - end - - def char - @char_code.chr(Encoding::UTF_8) - end - - def enhanced? - @enhanced - end - - # Verifies if the arguments match with this key event. - # Nil arguments are ignored, but at least one must be passed as non-nil. - # To verify that no control keys were pressed, pass an empty array: `control_keys: []`. - def matches?(control_keys: nil, virtual_key_code: nil, char_code: nil) - raise ArgumentError, 'No argument was passed to match key event' if control_keys.nil? && virtual_key_code.nil? && char_code.nil? - - (control_keys.nil? || [*control_keys].sort == @control_keys) && - (virtual_key_code.nil? || @virtual_key_code == virtual_key_code) && - (char_code.nil? || char_code == @char_code) - end - - end -end diff --git a/test/reline/helper.rb b/test/reline/helper.rb index 26fe834482..a5f850e838 100644 --- a/test/reline/helper.rb +++ b/test/reline/helper.rb @@ -22,29 +22,36 @@ module Reline class < expected but was - <#{chunk.inspect} (#{chunk.encoding.inspect})> in + <#{chunk.inspect} (#{chunk.encoding.inspect})> in EOM end diff --git a/test/reline/test_ansi_with_terminfo.rb b/test/reline/test_ansi_with_terminfo.rb index e1c56b9ee1..3adda10716 100644 --- a/test/reline/test_ansi_with_terminfo.rb +++ b/test/reline/test_ansi_with_terminfo.rb @@ -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 diff --git a/test/reline/test_ansi_without_terminfo.rb b/test/reline/test_ansi_without_terminfo.rb index 3d153514f3..2db14cf0a0 100644 --- a/test/reline/test_ansi_without_terminfo.rb +++ b/test/reline/test_ansi_without_terminfo.rb @@ -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 diff --git a/test/reline/test_config.rb b/test/reline/test_config.rb index 03e4178f32..8c4b513600 100644 --- a/test/reline/test_config.rb +++ b/test/reline/test_config.rb @@ -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 diff --git a/test/reline/test_line_editor.rb b/test/reline/test_line_editor.rb index 7a38ecd596..1859da8199 100644 --- a/test/reline/test_line_editor.rb +++ b/test/reline/test_line_editor.rb @@ -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 diff --git a/test/reline/test_reline.rb b/test/reline/test_reline.rb index a20a5c9f44..c664ab74b0 100644 --- a/test/reline/test_reline.rb +++ b/test/reline/test_reline.rb @@ -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(/#