Skip to content

Commit c898eda

Browse files
committed
Add console input event support on Windows
Preserve native console input event data for Windows consumers.
1 parent 3c19083 commit c898eda

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

ext/io/console/console.c

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,131 @@ console_set_winsize(VALUE io, VALUE size)
946946
#endif
947947

948948
#ifdef _WIN32
949+
typedef struct {
950+
HANDLE handle;
951+
INPUT_RECORD *records;
952+
DWORD length;
953+
DWORD count;
954+
BOOL result;
955+
} read_console_input_args_t;
956+
957+
static void *
958+
nogvl_read_console_input(void *ptr)
959+
{
960+
read_console_input_args_t *args = ptr;
961+
args->result = ReadConsoleInputW(args->handle, args->records, args->length, &args->count);
962+
return 0;
963+
}
964+
965+
static void
966+
console_input_event_set(VALUE event, const char *name, VALUE value)
967+
{
968+
rb_hash_aset(event, ID2SYM(rb_intern(name)), value);
969+
}
970+
971+
static VALUE
972+
console_input_event(const INPUT_RECORD *record)
973+
{
974+
VALUE event = rb_hash_new();
975+
976+
switch (record->EventType) {
977+
case KEY_EVENT:
978+
console_input_event_set(event, "type", ID2SYM(rb_intern("key")));
979+
console_input_event_set(event, "key_down", record->Event.KeyEvent.bKeyDown ? Qtrue : Qfalse);
980+
console_input_event_set(event, "repeat_count", UINT2NUM(record->Event.KeyEvent.wRepeatCount));
981+
console_input_event_set(event, "virtual_key_code", UINT2NUM(record->Event.KeyEvent.wVirtualKeyCode));
982+
console_input_event_set(event, "virtual_scan_code", UINT2NUM(record->Event.KeyEvent.wVirtualScanCode));
983+
console_input_event_set(event, "unicode_char", UINT2NUM(record->Event.KeyEvent.uChar.UnicodeChar));
984+
console_input_event_set(event, "control_key_state", UINT2NUM(record->Event.KeyEvent.dwControlKeyState));
985+
break;
986+
case MOUSE_EVENT:
987+
console_input_event_set(event, "type", ID2SYM(rb_intern("mouse")));
988+
console_input_event_set(event, "position", rb_assoc_new(
989+
INT2NUM(record->Event.MouseEvent.dwMousePosition.Y),
990+
INT2NUM(record->Event.MouseEvent.dwMousePosition.X)));
991+
console_input_event_set(event, "button_state", UINT2NUM(record->Event.MouseEvent.dwButtonState));
992+
console_input_event_set(event, "control_key_state", UINT2NUM(record->Event.MouseEvent.dwControlKeyState));
993+
console_input_event_set(event, "event_flags", UINT2NUM(record->Event.MouseEvent.dwEventFlags));
994+
break;
995+
case WINDOW_BUFFER_SIZE_EVENT:
996+
console_input_event_set(event, "type", ID2SYM(rb_intern("window_buffer_size")));
997+
console_input_event_set(event, "size", rb_assoc_new(
998+
INT2NUM(record->Event.WindowBufferSizeEvent.dwSize.Y),
999+
INT2NUM(record->Event.WindowBufferSizeEvent.dwSize.X)));
1000+
break;
1001+
case MENU_EVENT:
1002+
console_input_event_set(event, "type", ID2SYM(rb_intern("menu")));
1003+
console_input_event_set(event, "command_id", UINT2NUM(record->Event.MenuEvent.dwCommandId));
1004+
break;
1005+
case FOCUS_EVENT:
1006+
console_input_event_set(event, "type", ID2SYM(rb_intern("focus")));
1007+
console_input_event_set(event, "set_focus", record->Event.FocusEvent.bSetFocus ? Qtrue : Qfalse);
1008+
break;
1009+
default:
1010+
console_input_event_set(event, "type", UINT2NUM(record->EventType));
1011+
break;
1012+
}
1013+
1014+
return event;
1015+
}
1016+
1017+
/*
1018+
* call-seq:
1019+
* io.console_input_events([max_events]) -> array
1020+
*
1021+
* Reads up to +max_events+ console input events, preserving their order.
1022+
* The default is one event. Blocks until at least one event is available.
1023+
*
1024+
* Each event is returned as a Hash. The +:type+ and remaining keys are:
1025+
*
1026+
* - +:key+ : +:key_down+, +:repeat_count+, +:virtual_key_code+,
1027+
* +:virtual_scan_code+, +:unicode_char+, and +:control_key_state+.
1028+
* - +:mouse+ : +:position+ ([row, column]), +:button_state+,
1029+
* +:control_key_state+, and +:event_flags+.
1030+
* - +:window_buffer_size+ : +:size+ ([rows, columns]).
1031+
* - +:menu+ : +:command_id+.
1032+
* - +:focus+ : +:set_focus+.
1033+
*
1034+
* This method is Windows only.
1035+
*
1036+
* You must require 'io/console' to use this method.
1037+
*/
1038+
static VALUE
1039+
console_input_events(int argc, VALUE *argv, VALUE io)
1040+
{
1041+
VALUE vmax;
1042+
DWORD max_events = 1;
1043+
read_console_input_args_t args;
1044+
VALUE event_buffer = 0;
1045+
VALUE events;
1046+
DWORD i;
1047+
1048+
rb_scan_args(argc, argv, "01", &vmax);
1049+
if (!NIL_P(vmax)) {
1050+
max_events = NUM2UINT(vmax);
1051+
if (max_events == 0) rb_raise(rb_eArgError, "max_events must be positive");
1052+
}
1053+
1054+
args.handle = (HANDLE)rb_w32_get_osfhandle(GetReadFD(io));
1055+
args.records = ALLOCV_N(INPUT_RECORD, event_buffer, max_events);
1056+
args.length = max_events;
1057+
args.count = 0;
1058+
args.result = FALSE;
1059+
rb_thread_call_without_gvl(nogvl_read_console_input, &args, RUBY_UBF_IO, 0);
1060+
if (!args.result) {
1061+
int error = LAST_ERROR;
1062+
ALLOCV_END(event_buffer);
1063+
rb_syserr_fail(error, 0);
1064+
}
1065+
1066+
events = rb_ary_new_capa(args.count);
1067+
for (i = 0; i < args.count; ++i) {
1068+
rb_ary_push(events, console_input_event(&args.records[i]));
1069+
}
1070+
ALLOCV_END(event_buffer);
1071+
return events;
1072+
}
1073+
9491074
/*
9501075
* call-seq:
9511076
* io.check_winsize_changed { ... } -> io
@@ -974,6 +1099,7 @@ console_check_winsize_changed(VALUE io)
9741099
return io;
9751100
}
9761101
#else
1102+
#define console_input_events rb_f_notimplement
9771103
#define console_check_winsize_changed rb_f_notimplement
9781104
#endif
9791105

@@ -2076,6 +2202,7 @@ InitVM_console(void)
20762202
rb_define_method(rb_cIO, "scroll_backward", console_scroll_backward, 1);
20772203
rb_define_method(rb_cIO, "clear_screen", console_clear_screen, 0);
20782204
rb_define_method(rb_cIO, "pressed?", console_key_pressed_p, 1);
2205+
rb_define_method(rb_cIO, "console_input_events", console_input_events, -1);
20792206
rb_define_method(rb_cIO, "check_winsize_changed", console_check_winsize_changed, 0);
20802207
rb_define_method(rb_cIO, "getpass", console_getpass, -1);
20812208
rb_define_method(rb_cIO, "ttyname", console_ttyname, 0);

test/io/console/test_io_console.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,77 @@ def test_pressed_invalid
698698
end
699699
end
700700

701+
RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ and defined?(IO.console) and IO.console and \
702+
TestIO_Console.class_eval do
703+
def test_console_input_events
704+
require "fiddle"
705+
706+
kernel32 = Fiddle.dlopen("kernel32.dll")
707+
create_file = Fiddle::Function.new(
708+
kernel32["CreateFileW"],
709+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP,
710+
Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP],
711+
Fiddle::TYPE_INTPTR_T,
712+
)
713+
write_console_input = Fiddle::Function.new(
714+
kernel32["WriteConsoleInputW"],
715+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP],
716+
Fiddle::TYPE_INT,
717+
)
718+
close_handle = Fiddle::Function.new(
719+
kernel32["CloseHandle"],
720+
[Fiddle::TYPE_VOIDP],
721+
Fiddle::TYPE_INT,
722+
)
723+
724+
key = [1, 0, 1, 2, 0x41, 0x1e, 0x03a9, 0x18].pack("S<S<L<S<S<S<S<L<")
725+
resize = [4, 0, 120, 40].pack("S<S<s<s<").ljust(20, "\0")
726+
mouse = [2, 0, 7, 9, 1, 0x10, 2].pack("S<S<s<s<L<L<L<")
727+
menu = [8, 0, 123].pack("S<S<L<").ljust(20, "\0")
728+
focus = [16, 0, 1].pack("S<S<L<").ljust(20, "\0")
729+
written = [0].pack("L<")
730+
conin = "CONIN$\0".encode(Encoding::UTF_16LE)
731+
handle = create_file.call(conin, -0x40000000, 3, nil, 3, 0, nil)
732+
assert_not_equal(-1, handle)
733+
begin
734+
records = key + resize + mouse + menu + focus
735+
assert_not_equal(0, write_console_input.call(handle, records, 5, written))
736+
assert_equal(5, written.unpack1("L<"))
737+
ensure
738+
close_handle.call(handle)
739+
end
740+
741+
events = IO.console.console_input_events(128)
742+
index = events.index {|event| event[:type] == :key && event[:unicode_char] == 0x03a9}
743+
assert_not_nil(index)
744+
assert_equal(
745+
[
746+
{
747+
type: :key,
748+
key_down: true,
749+
repeat_count: 2,
750+
virtual_key_code: 0x41,
751+
virtual_scan_code: 0x1e,
752+
unicode_char: 0x03a9,
753+
control_key_state: 0x18,
754+
},
755+
{type: :window_buffer_size, size: [40, 120]},
756+
{
757+
type: :mouse,
758+
position: [9, 7],
759+
button_state: 1,
760+
control_key_state: 0x10,
761+
event_flags: 2,
762+
},
763+
{type: :menu, command_id: 123},
764+
{type: :focus, set_focus: true},
765+
],
766+
events[index, 5],
767+
)
768+
assert_raise(ArgumentError) {IO.console.console_input_events(0)}
769+
end
770+
end
771+
701772
class TestIO_Console
702773
def test_stringio_getch
703774
assert_ruby_status %w"--disable=gems -rstringio -rio/console", %q{

0 commit comments

Comments
 (0)