1+ require "stringio"
12require "io/console"
23require "fiddle"
34require "fiddle/import"
@@ -175,23 +176,26 @@ def repaint(width: @con.winsize[1], height: @con.winsize[0])
175176 ENABLE_QUICK_EDIT_MODE = 0x0040
176177 ENABLE_EXTENDED_FLAGS = 0x0080
177178 ENABLE_VIRTUAL_TERMINAL_INPUT = 0x200
179+ KEY_EVENT = 0x01
180+ MOUSE_EVENT = 0x02
181+ WINDOW_BUFFER_SIZE_EVENT = 0x04
178182
179183 attr_accessor :widget
180184
181185 def initialize
182186 @GetStdHandle = Win32API . new ( 'kernel32' , 'GetStdHandle' , [ 'L' ] , 'L' )
183187 @GetConsoleMode = Win32API . new ( 'kernel32' , 'GetConsoleMode' , [ 'L' , 'P' ] , 'L' )
184188 @SetConsoleMode = Win32API . new ( 'kernel32' , 'SetConsoleMode' , [ 'L' , 'L' ] , 'L' )
189+ @ReadConsoleInputW = Win32API . new ( 'kernel32' , 'ReadConsoleInputW' , [ 'L' , 'P' , 'L' , 'P' ] , 'L' )
190+ @GetConsoleScreenBufferInfo = Win32API . new ( 'kernel32' , 'GetConsoleScreenBufferInfo' , [ 'L' , 'P' ] , 'L' )
185191
186192 @hConsoleHandle = @GetStdHandle . call ( STD_INPUT_HANDLE )
187- @ev_r , @ev_w = IO . pipe . map ( &:binmode )
188- @read_request_queue = Thread ::Queue . new
193+ @hConsoleOutHandle = @GetStdHandle . call ( STD_OUTPUT_HANDLE )
189194
195+ @mouse_state = 0
196+ @old_winsize = IO . console . winsize
190197 set_consolemode
191198
192- register_term_size_change
193- register_stdin
194-
195199 at_exit do
196200 unset_consolemode
197201 end
@@ -240,49 +244,90 @@ def unset_consolemode
240244 call_with_console_handle ( @SetConsoleMode , mode )
241245 end
242246
243- private def register_term_size_change
244- if RUBY_PLATFORM =~ /mingw|mswin/
245- con = IO . console
246- old_size = con . winsize
247- Thread . new do
248- loop do
249- new_size = con . winsize
250- if old_size != new_size
251- old_size = new_size
252- @ev_w . write "\x01 "
253- end
254- sleep 1
255- end
256- end
247+ def get_console_screen_buffer_info
248+ # CONSOLE_SCREEN_BUFFER_INFO
249+ # [ 0,2] dwSize.X
250+ # [ 2,2] dwSize.Y
251+ # [ 4,2] dwCursorPositions.X
252+ # [ 6,2] dwCursorPositions.Y
253+ # [ 8,2] wAttributes
254+ # [10,2] srWindow.Left
255+ # [12,2] srWindow.Top
256+ # [14,2] srWindow.Right
257+ # [16,2] srWindow.Bottom
258+ # [18,2] dwMaximumWindowSize.X
259+ # [20,2] dwMaximumWindowSize.Y
260+ csbi = 0 . chr * 22
261+ if @GetConsoleScreenBufferInfo . call ( @hConsoleOutHandle , csbi ) != 0
262+ # returns [width, height, x, y, attributes, left, top, right, bottom]
263+ csbi . unpack ( "s9" )
257264 else
258- Signal . trap ( 'SIGWINCH' ) do
259- @ev_w . write "\x01 "
260- end
265+ return nil
261266 end
262267 end
263268
264- private def register_stdin
265- Thread . new do
266- str = +""
267- @read_request_queue . shift
268- c = IO . console
269- while char = c . read ( 1 )
270- str << char
271- next if !str . valid_encoding? ||
272- str == "\e " ||
273- str == "\e [" ||
274- str == "\xE0 " ||
275- str . match ( /\A \e \x5b <[0-9;]*\z / )
269+ private def winsize_changed?
270+ con = IO . console
271+ new_size = con . winsize
272+ if @old_winsize != new_size
273+ @old_winsize = new_size
274+ true
275+ else
276+ false
277+ end
278+ end
276279
277- @ev_w . write [ 2 , str . size , str ] . pack ( "CCa*" )
278- str = +""
279- @read_request_queue . shift
280+ def read_input_event
281+ # Wait for reception of at least one event
282+ input_records = 0 . chr * 20 * 1
283+ read_event = 0 . chr * 4
284+
285+ if @ReadConsoleInputW . ( @hConsoleHandle , input_records , 1 , read_event ) != 0
286+ read_events = read_event . unpack1 ( 'L' )
287+ 0 . upto ( read_events -1 ) do |idx |
288+ input_record = input_records [ idx * 20 , 20 ]
289+ event = input_record [ 0 , 2 ] . unpack1 ( 's*' )
290+ case event
291+ when KEY_EVENT
292+ key_down = input_record [ 4 , 4 ] . unpack1 ( 'l*' )
293+ repeat_count = input_record [ 8 , 2 ] . unpack1 ( 's*' )
294+ virtual_key_code = input_record [ 10 , 2 ] . unpack1 ( 's*' )
295+ virtual_scan_code = input_record [ 12 , 2 ] . unpack1 ( 's*' )
296+ char_code = input_record [ 14 , 2 ] . unpack1 ( 'S*' )
297+ control_key_state = input_record [ 16 , 2 ] . unpack1 ( 'S*' )
298+ is_key_down = key_down . zero? ? false : true
299+ if is_key_down
300+ # p [repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state]
301+
302+ return char_code . chr
303+ end
304+ when MOUSE_EVENT
305+ click_x , click_y , state = input_record [ 4 , 8 ] . unpack ( "ssL" )
306+ if @mouse_state != state
307+ # click state changed
308+ @mouse_state = state
309+ csbi = get_console_screen_buffer_info || raise ( "error at GetConsoleScreenBufferInfo" )
310+ click_y -= csbi [ 6 ]
311+ # p mouse: [click_x, click_y, state]
312+
313+ if state == 1
314+ # mouse button down
315+ return "\e \x5b <0;#{ click_x } ;#{ click_y } M"
316+ else
317+ # mouse button up
318+ return "\e \x5b <0;#{ click_x } ;#{ click_y } m"
319+ end
320+ end
321+ when WINDOW_BUFFER_SIZE_EVENT
322+ return :winsize_changed
323+ end
280324 end
281325 end
326+ false
282327 end
283328
284- private def request_read
285- @read_request_queue . push true
329+ private def windows_terminal?
330+ !! ENV [ "WT_SESSION" ]
286331 end
287332
288333 private def handle_key_input ( str )
@@ -299,35 +344,56 @@ def unset_consolemode
299344 unset_consolemode do
300345 widget . select
301346 end
302- when /\e \x5b <0;(\d +);(\d +)m/ # Mouse left button up
347+ when /\A \ e\x5b <0;(\d +);(\d +)m\z / # Mouse left button up
303348 if widget . click ( $1. to_i - 1 , $2. to_i - 2 )
304349 widget . repaint
305350 unset_consolemode do
306351 widget . select
307352 end
308353 end
309- when /\e \x5b <\d +;(\d +);(\d +)[Mm]/ # other mouse events
354+ when /\A \ e\x5b <\d +;(\d +);(\d +)[Mm]\z / # other mouse events
310355 return # no repaint
311356 end
312357 widget . repaint
313358 end
314359
315360 private def main_loop
316361 str = +""
317- request_read
318- while char = @ev_r . read ( 1 )
319- case char
320- when "\x01 "
321- widget . repaint
322- when "\x02 "
323- strlen = @ev_r . read ( 1 ) . unpack1 ( "C" )
324- str = @ev_r . read ( strlen )
325-
326- handle_key_input ( str )
362+ console_buffer = StringIO . new
363+ loop do
364+ if windows_terminal?
365+ c = IO . console
366+
367+ rs , = IO . select ( [ c ] , [ ] , [ ] , 0.5 )
368+ if rs
369+ char = c . read ( 1 )
370+ break unless char
371+ else
372+ # timeout -> check windows size change
373+ widget . repaint if winsize_changed?
374+ end
327375 else
328- raise "unexpected event: #{ char . inspect } "
376+ if console_buffer . eof?
377+ input = read_input_event
378+ if input == :winsize_changed
379+ widget . repaint if winsize_changed?
380+ elsif input
381+ console_buffer = StringIO . new ( input )
382+ end
383+ end
384+ char = console_buffer . read ( 1 )
329385 end
330- request_read
386+ next unless char
387+ str << char
388+
389+ next if !str . valid_encoding? ||
390+ str == "\e " ||
391+ str == "\e [" ||
392+ str == "\xE0 " ||
393+ str . match ( /\A \e \x5b <[0-9;]*\z / )
394+
395+ handle_key_input ( str )
396+ str = +""
331397 end
332398 end
333399
0 commit comments