|
| 1 | +require 'ffi' |
| 2 | + |
| 3 | +module IO::Console::Windows |
| 4 | + STD_INPUT_HANDLE = -10 |
| 5 | + STD_OUTPUT_HANDLE = -11 |
| 6 | + WAIT_OBJECT_0 = 0 |
| 7 | + WAIT_TIMEOUT = 258 |
| 8 | + ENABLE_WRAP_AT_EOL_OUTPUT = 2 |
| 9 | + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 |
| 10 | + |
| 11 | + module Native |
| 12 | + extend FFI::Library |
| 13 | + ffi_convention :stdcall |
| 14 | + ffi_lib 'kernel32' |
| 15 | + attach_function :GetStdHandle, [:int32], :pointer |
| 16 | + attach_function :GetConsoleMode, [:pointer, :pointer], :int |
| 17 | + attach_function :SetConsoleMode, [:pointer, :uint32], :int |
| 18 | + attach_function :WaitForSingleObject, [:pointer, :uint32], :uint32 |
| 19 | + attach_function :ReadConsoleInputW, |
| 20 | + [:pointer, :pointer, :uint32, :pointer], :int |
| 21 | + attach_function :GetFileType, [:pointer], :uint32 |
| 22 | + attach_function :GetFileInformationByHandleEx, |
| 23 | + [:pointer, :int, :pointer, :uint32], :int |
| 24 | + attach_function :GetConsoleScreenBufferInfo, [:pointer, :pointer], :int |
| 25 | + attach_function :SetConsoleCursorPosition, [:pointer, :uint32], :int |
| 26 | + attach_function :FillConsoleOutputCharacterW, |
| 27 | + [:pointer, :uint16, :uint32, :uint32, :pointer], :int |
| 28 | + attach_function :FillConsoleOutputAttribute, |
| 29 | + [:pointer, :uint16, :uint32, :uint32, :pointer], :int |
| 30 | + attach_function :GetConsoleCursorInfo, [:pointer, :pointer], :int |
| 31 | + attach_function :SetConsoleCursorInfo, [:pointer, :pointer], :int |
| 32 | + end |
| 33 | + |
| 34 | + module CRT |
| 35 | + extend FFI::Library |
| 36 | + ffi_convention :cdecl |
| 37 | + ffi_lib 'msvcrt' |
| 38 | + attach_function :_kbhit, [], :int |
| 39 | + end |
| 40 | + |
| 41 | + GetStdHandle = Native.method(:GetStdHandle) |
| 42 | + GetConsoleMode = Native.method(:GetConsoleMode) |
| 43 | + SetConsoleMode = Native.method(:SetConsoleMode) |
| 44 | + WaitForSingleObject = Native.method(:WaitForSingleObject) |
| 45 | + ReadConsoleInputW = Native.method(:ReadConsoleInputW) |
| 46 | + GetFileType = Native.method(:GetFileType) |
| 47 | + GetFileInformationByHandleEx = Native.method(:GetFileInformationByHandleEx) |
| 48 | + GetConsoleScreenBufferInfo = Native.method(:GetConsoleScreenBufferInfo) |
| 49 | + SetConsoleCursorPosition = Native.method(:SetConsoleCursorPosition) |
| 50 | + FillConsoleOutputCharacter = Native.method(:FillConsoleOutputCharacterW) |
| 51 | + FillConsoleOutputAttribute = Native.method(:FillConsoleOutputAttribute) |
| 52 | + GetConsoleCursorInfo = Native.method(:GetConsoleCursorInfo) |
| 53 | + SetConsoleCursorInfo = Native.method(:SetConsoleCursorInfo) |
| 54 | + Kbhit = CRT.method(:_kbhit) |
| 55 | + |
| 56 | + INPUT_HANDLE = GetStdHandle.call(STD_INPUT_HANDLE) |
| 57 | + OUTPUT_HANDLE = GetStdHandle.call(STD_OUTPUT_HANDLE) |
| 58 | + |
| 59 | + module_function |
| 60 | + |
| 61 | + def handle(io) |
| 62 | + io.equal?(STDIN) ? INPUT_HANDLE : OUTPUT_HANDLE |
| 63 | + end |
| 64 | + |
| 65 | + def console_mode(io) |
| 66 | + buffer = "\0" * 4 |
| 67 | + raise SystemCallError, 'GetConsoleMode' if GetConsoleMode.call(handle(io), buffer) == 0 |
| 68 | + buffer.unpack1('L') |
| 69 | + end |
| 70 | + |
| 71 | + def set_console_mode(io, mode) |
| 72 | + raise SystemCallError, 'SetConsoleMode' if SetConsoleMode.call(handle(io), mode) == 0 |
| 73 | + end |
| 74 | + |
| 75 | + def screen_buffer_info(io) |
| 76 | + buffer = "\0" * 22 |
| 77 | + raise SystemCallError, 'GetConsoleScreenBufferInfo' if GetConsoleScreenBufferInfo.call(handle(io), buffer) == 0 |
| 78 | + buffer.unpack('s9') |
| 79 | + end |
| 80 | + |
| 81 | + def coordinate(x, y) |
| 82 | + (y & 0xffff) << 16 | (x & 0xffff) |
| 83 | + end |
| 84 | +end |
| 85 | + |
| 86 | +module IO::Console::Windows::TTY |
| 87 | + def tty?(*types) |
| 88 | + return super() if types.empty? |
| 89 | + |
| 90 | + default = msys = cygwin = false |
| 91 | + types.each do |type| |
| 92 | + case type |
| 93 | + when nil |
| 94 | + default = true |
| 95 | + when :any |
| 96 | + default = msys = cygwin = true |
| 97 | + when :msys |
| 98 | + msys = true |
| 99 | + when :cygwin |
| 100 | + cygwin = true |
| 101 | + when Symbol |
| 102 | + raise ArgumentError, "unknown tty type: #{type.inspect}" |
| 103 | + else |
| 104 | + raise TypeError, "expected Symbol, got #{type.class}" |
| 105 | + end |
| 106 | + end |
| 107 | + |
| 108 | + return true if default && super() |
| 109 | + (msys || cygwin) && msys_tty?(msys, cygwin) |
| 110 | + end |
| 111 | + alias isatty tty? |
| 112 | + |
| 113 | + private def msys_tty?(msys, cygwin) |
| 114 | + windows = IO::Console::Windows |
| 115 | + handle = windows.handle(self) |
| 116 | + return false unless windows::GetFileType.call(handle) == 3 |
| 117 | + buffer = "\0" * 1024 |
| 118 | + return false if windows::GetFileInformationByHandleEx.call(handle, 2, buffer, 1022) == 0 |
| 119 | + length = buffer.unpack1('L') |
| 120 | + name = buffer[4, length].encode(Encoding::UTF_8, Encoding::UTF_16LE, invalid: :replace) |
| 121 | + return false unless (msys && name.start_with?('\\msys-')) || (cygwin && name.start_with?('\\cygwin-')) |
| 122 | + name.include?('-pty') |
| 123 | + end |
| 124 | +end |
| 125 | + |
| 126 | +IO.prepend(IO::Console::Windows::TTY) |
| 127 | + |
| 128 | +class IO::ConsoleMode |
| 129 | + def initialize(mode) |
| 130 | + @mode = mode |
| 131 | + end |
| 132 | + |
| 133 | + def virtual_terminal_processing? |
| 134 | + @mode & IO::Console::Windows::ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 |
| 135 | + end |
| 136 | + |
| 137 | + def virtual_terminal_processing=(enabled) |
| 138 | + set_flag(IO::Console::Windows::ENABLE_VIRTUAL_TERMINAL_PROCESSING, enabled) |
| 139 | + end |
| 140 | + |
| 141 | + def wrap_at_eol_output? |
| 142 | + @mode & IO::Console::Windows::ENABLE_WRAP_AT_EOL_OUTPUT != 0 |
| 143 | + end |
| 144 | + |
| 145 | + def wrap_at_eol_output=(enabled) |
| 146 | + set_flag(IO::Console::Windows::ENABLE_WRAP_AT_EOL_OUTPUT, enabled) |
| 147 | + end |
| 148 | + |
| 149 | + private def set_flag(flag, enabled) |
| 150 | + enabled ? @mode |= flag : @mode &= ~flag |
| 151 | + self |
| 152 | + end |
| 153 | + |
| 154 | + private def to_i |
| 155 | + @mode |
| 156 | + end |
| 157 | +end |
| 158 | + |
| 159 | +class IO |
| 160 | + def console_mode |
| 161 | + IO::ConsoleMode.new(IO::Console::Windows.console_mode(self)) |
| 162 | + end |
| 163 | + |
| 164 | + def console_mode=(mode) |
| 165 | + IO::Console::Windows.set_console_mode(self, mode.__send__(:to_i)) |
| 166 | + mode |
| 167 | + end |
| 168 | + |
| 169 | + def input_pending? |
| 170 | + windows = IO::Console::Windows |
| 171 | + return windows::Kbhit.call != 0 if windows::GetConsoleMode.call(windows.handle(self), "\0" * 4) != 0 |
| 172 | + respond_to?(:wait_readable) && !!wait_readable(0) |
| 173 | + end |
| 174 | + |
| 175 | + def console_input_events(max_events = 1, timeout: nil) |
| 176 | + raise ArgumentError, 'max_events must be positive' unless max_events > 0 |
| 177 | + raise ArgumentError, 'time interval must not be negative' if timeout && timeout < 0 |
| 178 | + |
| 179 | + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout |
| 180 | + loop do |
| 181 | + wait = deadline && deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) |
| 182 | + return [] if wait && wait <= 0 |
| 183 | + milliseconds = wait ? [[(wait * 1000).ceil, 100].min, 0].max : 100 |
| 184 | + windows = IO::Console::Windows |
| 185 | + result = windows::WaitForSingleObject.call(windows.handle(self), milliseconds) |
| 186 | + break if result == windows::WAIT_OBJECT_0 |
| 187 | + return [] if result != windows::WAIT_TIMEOUT |
| 188 | + return [] if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline |
| 189 | + end |
| 190 | + |
| 191 | + records = "\0" * 20 * max_events |
| 192 | + count = "\0" * 4 |
| 193 | + windows = IO::Console::Windows |
| 194 | + if windows::ReadConsoleInputW.call(windows.handle(self), records, max_events, count) == 0 |
| 195 | + raise SystemCallError, 'ReadConsoleInputW' |
| 196 | + end |
| 197 | + count.unpack1('L').times.map do |index| |
| 198 | + record = records[index * 20, 20] |
| 199 | + event_type = record.unpack1('S') |
| 200 | + case event_type |
| 201 | + when 1 |
| 202 | + key_down, repeat_count, virtual_key_code, virtual_scan_code, |
| 203 | + unicode_char, control_key_state = record[4, 16].unpack('LS4L') |
| 204 | + { |
| 205 | + type: :key, key_down: key_down != 0, repeat_count: repeat_count, |
| 206 | + virtual_key_code: virtual_key_code, virtual_scan_code: virtual_scan_code, |
| 207 | + unicode_char: unicode_char, control_key_state: control_key_state, |
| 208 | + } |
| 209 | + when 2 |
| 210 | + x, y, button_state, control_key_state, event_flags = record[4, 16].unpack('s2L3') |
| 211 | + {type: :mouse, position: [y, x], button_state: button_state, |
| 212 | + control_key_state: control_key_state, event_flags: event_flags} |
| 213 | + when 4 |
| 214 | + x, y = record[4, 4].unpack('s2') |
| 215 | + {type: :window_buffer_size, size: [y, x]} |
| 216 | + when 8 |
| 217 | + {type: :menu, command_id: record[4, 4].unpack1('L')} |
| 218 | + when 16 |
| 219 | + {type: :focus, set_focus: record[4, 4].unpack1('L') != 0} |
| 220 | + else |
| 221 | + {type: event_type} |
| 222 | + end |
| 223 | + end |
| 224 | + end |
| 225 | + |
| 226 | + def winsize |
| 227 | + width, _, _, _, _, _, top, _, bottom = IO::Console::Windows.screen_buffer_info(self) |
| 228 | + [bottom - top + 1, width] |
| 229 | + end |
| 230 | + |
| 231 | + def cursor |
| 232 | + _, _, x, y, _, _, top, = IO::Console::Windows.screen_buffer_info(self) |
| 233 | + [y - top, x] |
| 234 | + end |
| 235 | + |
| 236 | + def goto(row, column) |
| 237 | + windows = IO::Console::Windows |
| 238 | + _, _, _, _, _, _, top, = windows.screen_buffer_info(self) |
| 239 | + position = windows.coordinate(column, row + top) |
| 240 | + raise SystemCallError, 'SetConsoleCursorPosition' if windows::SetConsoleCursorPosition.call(windows.handle(self), position) == 0 |
| 241 | + self |
| 242 | + end |
| 243 | + |
| 244 | + def goto_column(column) |
| 245 | + row, = cursor |
| 246 | + goto(row, column) |
| 247 | + end |
| 248 | + |
| 249 | + def erase_line(mode) |
| 250 | + raise ArgumentError, 'invalid line erase mode' unless (0..2).cover?(mode) |
| 251 | + windows = IO::Console::Windows |
| 252 | + width, _, x, y, attributes, = windows.screen_buffer_info(self) |
| 253 | + start = mode == 0 ? x : 0 |
| 254 | + length = mode == 0 ? width - x : mode == 1 ? x + 1 : width |
| 255 | + position = windows.coordinate(start, y) |
| 256 | + written = "\0" * 4 |
| 257 | + windows::FillConsoleOutputCharacter.call(windows.handle(self), 0x20, length, position, written) |
| 258 | + windows::FillConsoleOutputAttribute.call(windows.handle(self), attributes, length, position, written) |
| 259 | + self |
| 260 | + end |
| 261 | + |
| 262 | + def clear_screen |
| 263 | + windows = IO::Console::Windows |
| 264 | + width, _, _, _, attributes, _, top, _, bottom = windows.screen_buffer_info(self) |
| 265 | + length = width * (bottom - top + 1) |
| 266 | + position = windows.coordinate(0, top) |
| 267 | + written = "\0" * 4 |
| 268 | + windows::FillConsoleOutputCharacter.call(windows.handle(self), 0x20, length, position, written) |
| 269 | + windows::FillConsoleOutputAttribute.call(windows.handle(self), attributes, length, position, written) |
| 270 | + windows::SetConsoleCursorPosition.call(windows.handle(self), position) |
| 271 | + self |
| 272 | + end |
| 273 | + |
| 274 | + def hide_cursor |
| 275 | + set_cursor_visibility(false) |
| 276 | + end |
| 277 | + |
| 278 | + def show_cursor |
| 279 | + set_cursor_visibility(true) |
| 280 | + end |
| 281 | + |
| 282 | + private def set_cursor_visibility(visible) |
| 283 | + info = "\0" * 8 |
| 284 | + windows = IO::Console::Windows |
| 285 | + handle = windows.handle(self) |
| 286 | + raise SystemCallError, 'GetConsoleCursorInfo' if windows::GetConsoleCursorInfo.call(handle, info) == 0 |
| 287 | + size, = info.unpack('L2') |
| 288 | + info = [size, visible ? 1 : 0].pack('L2') |
| 289 | + raise SystemCallError, 'SetConsoleCursorInfo' if windows::SetConsoleCursorInfo.call(handle, info) == 0 |
| 290 | + self |
| 291 | + end |
| 292 | + |
| 293 | +end |
0 commit comments