diff options
Diffstat (limited to 'lib/reline/windows.rb')
-rw-r--r-- | lib/reline/windows.rb | 316 |
1 files changed, 228 insertions, 88 deletions
diff --git a/lib/reline/windows.rb b/lib/reline/windows.rb index 072fb1b6cd..ee3f73e383 100644 --- a/lib/reline/windows.rb +++ b/lib/reline/windows.rb @@ -1,6 +1,8 @@ require 'fiddle/import' class Reline::Windows + RESET_COLOR = "\e[0m" + def self.encoding Encoding::UTF_8 end @@ -42,6 +44,14 @@ class Reline::Windows }.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 @@ -77,7 +87,7 @@ class Reline::Windows 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 == 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) @@ -87,10 +97,11 @@ class Reline::Windows end VK_RETURN = 0x0D - VK_MENU = 0x12 + 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 @@ -105,6 +116,7 @@ class Reline::Windows SCROLLLOCK_ON = 0x0040 SHIFT_PRESSED = 0x0010 + VK_TAB = 0x09 VK_END = 0x23 VK_HOME = 0x24 VK_LEFT = 0x25 @@ -132,9 +144,11 @@ class Reline::Windows @@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 @@ -156,7 +170,9 @@ class Reline::Windows @@input_buf = [] @@output_buf = [] - def self.msys_tty?(io=@@hConsoleInputHandle) + @@output = STDOUT + + def self.msys_tty?(io = @@hConsoleInputHandle) # check if fd is a pipe if @@GetFileType.call(io) != FILE_TYPE_PIPE return false @@ -172,7 +188,7 @@ class Reline::Windows # DWORD FileNameLength; # WCHAR FileName[1]; # } FILE_NAME_INFO - len = p_buffer[0, 4].unpack("L")[0] + 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') @@ -180,76 +196,108 @@ class Reline::Windows 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) - char = char_code.chr(Encoding::UTF_8) - if char_code == 0x0D and control_key_state.anybits?(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED | SHIFT_PRESSED) - # It's treated as Meta+Enter on Windows. - @@output_buf.push("\e".ord) - @@output_buf.push(char_code) - elsif char_code == 0x20 and control_key_state.anybits?(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) - # It's treated as Meta+Space on Windows. - @@output_buf.push("\e".ord) - @@output_buf.push(char_code) - elsif control_key_state.anybits?(LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) - @@output_buf.push("\e".ord) - @@output_buf.concat(char.bytes) - elsif control_key_state.anybits?(ENHANCED_KEY) - case virtual_key_code # Emulate getwch() key sequences. - when VK_END - @@output_buf.push(0, 79) - when VK_HOME - @@output_buf.push(0, 71) - when VK_LEFT - @@output_buf.push(0, 75) - when VK_UP - @@output_buf.push(0, 72) - when VK_RIGHT - @@output_buf.push(0, 77) - when VK_DOWN - @@output_buf.push(0, 80) - when VK_DELETE - @@output_buf.push(0, 83) - end - elsif char_code == 0 and control_key_state != 0 - # unknown - else - case virtual_key_code - when VK_RETURN - @@output_buf.push("\n".ord) + + # 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 - @@output_buf.concat(char.bytes) + # 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? #or true - next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack('L').first == 0 - input_record = 0.chr * 18 + 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_record, 1, read_event) != 0 - event = input_record[0, 2].unpack('s*').first - case event - when WINDOW_BUFFER_SIZE_EVENT - @@winch_handler.() - when KEY_EVENT - key_down = input_record[4, 4].unpack('l*').first - repeat_count = input_record[8, 2].unpack('s*').first - virtual_key_code = input_record[10, 2].unpack('s*').first - virtual_scan_code = input_record[12, 2].unpack('s*').first - char_code = input_record[14, 2].unpack('S*').first - control_key_state = input_record[16, 2].unpack('S*').first - 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) + 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.getc + def self.with_raw_input + yield + end + + def self.getc(_timeout_second) check_input_event @@output_buf.shift end @@ -263,7 +311,7 @@ class Reline::Windows end def self.empty_buffer? - if not @@input_buf.empty? + if not @@output_buf.empty? false elsif @@kbhit.call == 0 true @@ -272,17 +320,37 @@ class Reline::Windows end end - def self.get_screen_size + 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 - @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) + 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 - csbi = 0.chr * 22 - @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) - x = csbi[4, 2].unpack('s*').first - y = csbi[6, 2].unpack('s*').first + 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 @@ -302,6 +370,7 @@ class Reline::Windows 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) @@ -312,42 +381,74 @@ class Reline::Windows end def self.erase_after_cursor - csbi = 0.chr * 24 - @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) - cursor = csbi[4, 4].unpack('L').first + 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, 0, 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.zero? - screen_height = get_screen_size.first - val = screen_height - 1 if val > (screen_height - 1) - scroll_rectangle = [0, val, get_screen_size.last, get_screen_size.first].pack('s4') - destination_origin = 0 # y * 65536 + x - fill = [' '.ord, 0].pack('SS') - @@ScrollConsoleScreenBuffer.call(@@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill) + 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 - csbi = 0.chr * 22 - return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0 - buffer_width = csbi[0, 2].unpack('S').first - attributes = csbi[8, 2].unpack('S').first - _window_left, window_top, _window_right, window_bottom = *csbi[10,8].unpack('S*') - 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) + 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 @@ -360,4 +461,43 @@ class Reline::Windows 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 |