Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 119 additions & 53 deletions lib/ruby_installer/runtime/console_ui.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "stringio"
require "io/console"
require "fiddle"
require "fiddle/import"
Expand Down Expand Up @@ -175,23 +176,26 @@ def repaint(width: @con.winsize[1], height: @con.winsize[0])
ENABLE_QUICK_EDIT_MODE = 0x0040
ENABLE_EXTENDED_FLAGS = 0x0080
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x200
KEY_EVENT = 0x01
MOUSE_EVENT = 0x02
WINDOW_BUFFER_SIZE_EVENT = 0x04

attr_accessor :widget

def initialize
@GetStdHandle = Win32API.new('kernel32', 'GetStdHandle', ['L'], 'L')
@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
@GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo', ['L', 'P'], 'L')

@hConsoleHandle = @GetStdHandle.call(STD_INPUT_HANDLE)
@ev_r, @ev_w = IO.pipe.map(&:binmode)
@read_request_queue = Thread::Queue.new
@hConsoleOutHandle = @GetStdHandle.call(STD_OUTPUT_HANDLE)

@mouse_state = 0
@old_winsize = IO.console.winsize
set_consolemode

register_term_size_change
register_stdin

at_exit do
unset_consolemode
end
Expand Down Expand Up @@ -240,49 +244,90 @@ def unset_consolemode
call_with_console_handle(@SetConsoleMode, mode)
end

private def register_term_size_change
if RUBY_PLATFORM =~ /mingw|mswin/
con = IO.console
old_size = con.winsize
Thread.new do
loop do
new_size = con.winsize
if old_size != new_size
old_size = new_size
@ev_w.write "\x01"
end
sleep 1
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
if @GetConsoleScreenBufferInfo.call(@hConsoleOutHandle, csbi) != 0
# returns [width, height, x, y, attributes, left, top, right, bottom]
csbi.unpack("s9")
else
Signal.trap('SIGWINCH') do
@ev_w.write "\x01"
end
return nil
end
end

private def register_stdin
Thread.new do
str = +""
@read_request_queue.shift
c = IO.console
while char=c.read(1)
str << char
next if !str.valid_encoding? ||
str == "\e" ||
str == "\e[" ||
str == "\xE0" ||
str.match(/\A\e\x5b<[0-9;]*\z/)
private def winsize_changed?
con = IO.console
new_size = con.winsize
if @old_winsize != new_size
@old_winsize = new_size
true
else
false
end
end

@ev_w.write [2, str.size, str].pack("CCa*")
str = +""
@read_request_queue.shift
def read_input_event
# Wait for reception of at least one event
input_records = 0.chr * 20 * 1
read_event = 0.chr * 4

if @ReadConsoleInputW.(@hConsoleHandle, input_records, 1, read_event) != 0
read_events = read_event.unpack1('L')
0.upto(read_events-1) do |idx|
input_record = input_records[idx * 20, 20]
event = input_record[0, 2].unpack1('s*')
case event
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
# p [repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state]

return char_code.chr
end
when MOUSE_EVENT
click_x, click_y, state = input_record[4, 8].unpack("ssL")
if @mouse_state != state
# click state changed
@mouse_state = state
csbi = get_console_screen_buffer_info || raise("error at GetConsoleScreenBufferInfo")
click_y -= csbi[6]
# p mouse: [click_x, click_y, state]

if state == 1
# mouse button down
return "\e\x5b<0;#{click_x};#{click_y}M"
else
# mouse button up
return "\e\x5b<0;#{click_x};#{click_y}m"
end
end
when WINDOW_BUFFER_SIZE_EVENT
return :winsize_changed
end
end
end
false
end

private def request_read
@read_request_queue.push true
private def windows_terminal?
!!ENV["WT_SESSION"]
end

private def handle_key_input(str)
Expand All @@ -299,35 +344,56 @@ def unset_consolemode
unset_consolemode do
widget.select
end
when /\e\x5b<0;(\d+);(\d+)m/ # Mouse left button up
when /\A\e\x5b<0;(\d+);(\d+)m\z/ # Mouse left button up
if widget.click($1.to_i - 1, $2.to_i - 2)
widget.repaint
unset_consolemode do
widget.select
end
end
when /\e\x5b<\d+;(\d+);(\d+)[Mm]/ # other mouse events
when /\A\e\x5b<\d+;(\d+);(\d+)[Mm]\z/ # other mouse events
return # no repaint
end
widget.repaint
end

private def main_loop
str = +""
request_read
while char=@ev_r.read(1)
case char
when "\x01"
widget.repaint
when "\x02"
strlen = @ev_r.read(1).unpack1("C")
str = @ev_r.read(strlen)

handle_key_input(str)
console_buffer = StringIO.new
loop do
if windows_terminal?
c = IO.console

rs, = IO.select([c], [], [], 0.5)
if rs
char = c.read(1)
break unless char
else
# timeout -> check windows size change
widget.repaint if winsize_changed?
end
else
raise "unexpected event: #{char.inspect}"
if console_buffer.eof?
input = read_input_event
if input == :winsize_changed
widget.repaint if winsize_changed?
elsif input
console_buffer = StringIO.new(input)
end
end
char = console_buffer.read(1)
end
request_read
next unless char
str << char

next if !str.valid_encoding? ||
str == "\e" ||
str == "\e[" ||
str == "\xE0" ||
str.match(/\A\e\x5b<[0-9;]*\z/)

handle_key_input(str)
str = +""
end
end

Expand Down
2 changes: 1 addition & 1 deletion resources/files/startmenu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

bt = <<~EOT
irb:>
Expand Down
Loading