Skip to content

Commit 2204881

Browse files
committed
Add mouse event handling to classic terminal and avoid threads
Using `ReadConsoleInput` the mouse inputs work for conhost.exe as well. Stopping the threads when calling out per `Kernel.system` is a problem, since they continue to run and eat the input characters of sub-processes. This can be avoided by not using threads in favour of a single main_loop.
1 parent e59cc94 commit 2204881

File tree

2 files changed

+120
-54
lines changed

2 files changed

+120
-54
lines changed

lib/ruby_installer/runtime/console_ui.rb

Lines changed: 119 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require "stringio"
12
require "io/console"
23
require "fiddle"
34
require "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

resources/files/startmenu.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
app = RubyInstaller::Runtime::ConsoleUi.new
44
bm = RubyInstaller::Runtime::ConsoleUi::ButtonMatrix.new ncols: 3
5-
bm.headline = "Ruby startmenu - Choose item by #{ENV["WT_SESSION"] && "mouse or "}cursor keys and press Enter"
5+
bm.headline = "Ruby startmenu - Choose item by mouse or cursor keys and press Enter"
66

77
bt = <<~EOT
88
irb:>

0 commit comments

Comments
 (0)