diff --git a/samples/2048.cr b/samples/2048.cr index 081721decccf..6e3d294e864c 100644 --- a/samples/2048.cr +++ b/samples/2048.cr @@ -380,4 +380,5 @@ class Game end end +at_exit { STDIN.cooked! } Game.new.run diff --git a/spec/std/io/io_spec.cr b/spec/std/io/io_spec.cr index ccd362b73b4e..011960296cb9 100644 --- a/spec/std/io/io_spec.cr +++ b/spec/std/io/io_spec.cr @@ -958,8 +958,14 @@ describe IO do end end - {% unless flag?(:win32) %} - typeof(STDIN.cooked { }) - typeof(STDIN.cooked!) + typeof(STDIN.noecho { }) + typeof(STDIN.noecho!) + {% if flag?(:win32) %} + typeof(STDIN.echo { }) + typeof(STDIN.echo!) {% end %} + typeof(STDIN.cooked { }) + typeof(STDIN.cooked!) + typeof(STDIN.raw { }) + typeof(STDIN.raw!) end diff --git a/src/crystal/system/win32/file_descriptor.cr b/src/crystal/system/win32/file_descriptor.cr index 62784bf4abee..a99e911b5ab8 100644 --- a/src/crystal/system/win32/file_descriptor.cr +++ b/src/crystal/system/win32/file_descriptor.cr @@ -195,6 +195,43 @@ module Crystal::System::FileDescriptor end io end + + private def system_echo(enable : Bool, & : ->) + system_console_mode(enable, LibC::ENABLE_ECHO_INPUT, 0) { yield } + end + + private def system_raw(enable : Bool, & : ->) + system_console_mode(enable, LibC::ENABLE_VIRTUAL_TERMINAL_INPUT, LibC::ENABLE_PROCESSED_INPUT | LibC::ENABLE_LINE_INPUT | LibC::ENABLE_ECHO_INPUT) { yield } + end + + @[AlwaysInline] + private def system_console_mode(enable, on_mask, off_mask) + windows_handle = self.windows_handle + if LibC.GetConsoleMode(windows_handle, out old_mode) == 0 + raise IO::Error.from_winerror("GetConsoleMode") + end + + old_on_bits = old_mode & on_mask + old_off_bits = old_mode & off_mask + if enable + return yield if old_on_bits == on_mask && old_off_bits == 0 + new_mode = (old_mode | on_mask) & ~off_mask + else + return yield if old_on_bits == 0 && old_off_bits == off_mask + new_mode = (old_mode | off_mask) & ~on_mask + end + + if LibC.SetConsoleMode(windows_handle, new_mode) == 0 + raise IO::Error.from_winerror("SetConsoleMode") + end + + ret = yield + if LibC.GetConsoleMode(windows_handle, pointerof(old_mode)) != 0 + new_mode = (old_mode & ~on_mask & ~off_mask) | old_on_bits | old_off_bits + LibC.SetConsoleMode(windows_handle, new_mode) + end + ret + end end # Enable UTF-8 console I/O for the duration of program execution diff --git a/src/io/console.cr b/src/io/console.cr index a5ea2e52530c..6ad477392cbc 100644 --- a/src/io/console.cr +++ b/src/io/console.cr @@ -1,116 +1,208 @@ -{% skip_file if flag?(:win32) %} - -require "termios" - -class IO::FileDescriptor < IO - # Turns off character echoing for the duration of the given block. - # This will prevent displaying back to the user what they enter on the terminal. - # Only call this when this IO is a TTY, such as a not redirected stdin. - # - # ``` - # print "Enter password: " - # password = STDIN.noecho &.gets.try &.chomp - # puts - # ``` - def noecho - preserving_tc_mode("can't set IO#noecho") do |mode| - noecho_from_tc_mode! - yield self +{% if flag?(:win32) %} + class IO::FileDescriptor < IO + # Yields `self` to the given block, disables character echoing for the + # duration of the block, and returns the block's value. + # + # This will prevent displaying back to the user what they enter on the terminal. + # + # Raises `IO::Error` if this `IO` is not a terminal device. + # + # ``` + # print "Enter password: " + # password = STDIN.noecho &.gets.try &.chomp + # puts + # ``` + def noecho(& : self -> _) + system_echo(false) { yield self } end - end - # Turns off character echoing for this IO. - # This will prevent displaying back to the user what they enter on the terminal. - # Only call this when this IO is a TTY, such as a not redirected stdin. - def noecho! - if LibC.tcgetattr(fd, out mode) != 0 - raise IO::Error.from_errno "can't set IO#noecho!" + # Yields `self` to the given block, enables character echoing for the + # duration of the block, and returns the block's value. + # + # This causes user input to be displayed as they are entered on the terminal. + # + # Raises `IO::Error` if this `IO` is not a terminal device. + def echo(& : self -> _) + system_echo(true) { yield self } end - noecho_from_tc_mode! - end - macro noecho_from_tc_mode! - mode.c_lflag &= ~(Termios::LocalMode.flags(ECHO, ECHOE, ECHOK, ECHONL).value) - LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) - end + # Disables character echoing on this `IO`. + # + # This will prevent displaying back to the user what they enter on the terminal. + # + # Raises `IO::Error` if this `IO` is not a terminal device. + def noecho! : Nil + system_echo(false) { return } + end - # Enables character processing for the duration of the given block. - # The so called cooked mode is the standard behavior of a terminal, - # doing line wise editing by the terminal and only sending the input to - # the program on a newline. - # Only call this when this IO is a TTY, such as a not redirected stdin. - def cooked - preserving_tc_mode("can't set IO#cooked") do |mode| - cooked_from_tc_mode! - yield self + # Enables character echoing on this `IO`. + # + # This causes user input to be displayed as they are entered on the terminal. + # + # Raises `IO::Error` if this `IO` is not a terminal device. + def echo! : Nil + system_echo(true) { return } end - end - # Enables character processing for this IO. - # The so called cooked mode is the standard behavior of a terminal, - # doing line wise editing by the terminal and only sending the input to - # the program on a newline. - # Only call this when this IO is a TTY, such as a not redirected stdin. - def cooked! : Nil - if LibC.tcgetattr(fd, out mode) != 0 - raise IO::Error.from_errno "can't set IO#cooked!" + # Yields `self` to the given block, enables character processing for the + # duration of the block, and returns the block's value. + # + # The so called cooked mode is the standard behavior of a terminal, + # doing line wise editing by the terminal and only sending the input to + # the program on a newline. + # + # Raises `IO::Error` if this `IO` is not a terminal device. + def cooked(& : self -> _) + system_raw(false) { yield self } end - cooked_from_tc_mode! - end - macro cooked_from_tc_mode! - mode.c_iflag |= (Termios::InputMode::BRKINT | - Termios::InputMode::ISTRIP | - Termios::InputMode::ICRNL | - Termios::InputMode::IXON).value - mode.c_oflag |= Termios::OutputMode::OPOST.value - mode.c_lflag |= (Termios::LocalMode::ECHO | - Termios::LocalMode::ECHOE | - Termios::LocalMode::ECHOK | - Termios::LocalMode::ECHONL | - Termios::LocalMode::ICANON | - Termios::LocalMode::ISIG | - Termios::LocalMode::IEXTEN).value - LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) - end + # Yields `self` to the given block, enables raw mode for the duration of the + # block, and returns the block's value. + # + # In raw mode every keypress is directly sent to the program, no interpretation + # is done by the terminal. On Windows, this also enables ANSI input escape + # sequences. + # + # Raises `IO::Error` if this `IO` is not a terminal device. + def raw(& : self -> _) + system_raw(true) { yield self } + end - # Enables raw mode for the duration of the given block. - # In raw mode every keypress is directly sent to the program, no interpretation - # is done by the terminal. - # Only call this when this IO is a TTY, such as a not redirected stdin. - def raw - preserving_tc_mode("can't set IO#raw") do |mode| - raw_from_tc_mode! - yield self + # Enables character processing on this `IO`. + # + # The so called cooked mode is the standard behavior of a terminal, + # doing line wise editing by the terminal and only sending the input to + # the program on a newline. + # + # Raises `IO::Error` if this `IO` is not a terminal device. + def cooked! : Nil + system_raw(false) { return } + end + + # Enables raw mode on this `IO`. + # + # In raw mode every keypress is directly sent to the program, no interpretation + # is done by the terminal. On Windows, this also enables ANSI input escape + # sequences. + # + # Raises `IO::Error` if this `IO` is not a terminal device. + def raw! : Nil + system_raw(true) { return } end end +{% else %} + require "termios" - # Enables raw mode for this IO. - # In raw mode every keypress is directly sent to the program, no interpretation - # is done by the terminal. - # Only call this when this IO is a TTY, such as a not redirected stdin. - def raw! - if LibC.tcgetattr(fd, out mode) != 0 - raise IO::Error.from_errno "can't set IO#raw!" + class IO::FileDescriptor < IO + # Turns off character echoing for the duration of the given block. + # This will prevent displaying back to the user what they enter on the terminal. + # Only call this when this IO is a TTY, such as a not redirected stdin. + # + # ``` + # print "Enter password: " + # password = STDIN.noecho &.gets.try &.chomp + # puts + # ``` + def noecho + preserving_tc_mode("can't set IO#noecho") do |mode| + noecho_from_tc_mode! + yield self + end end - raw_from_tc_mode! - end + # Turns off character echoing for this IO. + # This will prevent displaying back to the user what they enter on the terminal. + # Only call this when this IO is a TTY, such as a not redirected stdin. + def noecho! + if LibC.tcgetattr(fd, out mode) != 0 + raise IO::Error.from_errno "can't set IO#noecho!" + end + noecho_from_tc_mode! + end - macro raw_from_tc_mode! - LibC.cfmakeraw(pointerof(mode)) - LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) - end + macro noecho_from_tc_mode! + mode.c_lflag &= ~(Termios::LocalMode.flags(ECHO, ECHOE, ECHOK, ECHONL).value) + LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) + end + + # Enables character processing for the duration of the given block. + # The so called cooked mode is the standard behavior of a terminal, + # doing line wise editing by the terminal and only sending the input to + # the program on a newline. + # Only call this when this IO is a TTY, such as a not redirected stdin. + def cooked + preserving_tc_mode("can't set IO#cooked") do |mode| + cooked_from_tc_mode! + yield self + end + end + + # Enables character processing for this IO. + # The so called cooked mode is the standard behavior of a terminal, + # doing line wise editing by the terminal and only sending the input to + # the program on a newline. + # Only call this when this IO is a TTY, such as a not redirected stdin. + def cooked! : Nil + if LibC.tcgetattr(fd, out mode) != 0 + raise IO::Error.from_errno "can't set IO#cooked!" + end + cooked_from_tc_mode! + end + + macro cooked_from_tc_mode! + mode.c_iflag |= (Termios::InputMode::BRKINT | + Termios::InputMode::ISTRIP | + Termios::InputMode::ICRNL | + Termios::InputMode::IXON).value + mode.c_oflag |= Termios::OutputMode::OPOST.value + mode.c_lflag |= (Termios::LocalMode::ECHO | + Termios::LocalMode::ECHOE | + Termios::LocalMode::ECHOK | + Termios::LocalMode::ECHONL | + Termios::LocalMode::ICANON | + Termios::LocalMode::ISIG | + Termios::LocalMode::IEXTEN).value + LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) + end + + # Enables raw mode for the duration of the given block. + # In raw mode every keypress is directly sent to the program, no interpretation + # is done by the terminal. + # Only call this when this IO is a TTY, such as a not redirected stdin. + def raw + preserving_tc_mode("can't set IO#raw") do |mode| + raw_from_tc_mode! + yield self + end + end + + # Enables raw mode for this IO. + # In raw mode every keypress is directly sent to the program, no interpretation + # is done by the terminal. + # Only call this when this IO is a TTY, such as a not redirected stdin. + def raw! + if LibC.tcgetattr(fd, out mode) != 0 + raise IO::Error.from_errno "can't set IO#raw!" + end - private def preserving_tc_mode(msg) - if LibC.tcgetattr(fd, out mode) != 0 - raise IO::Error.from_errno msg + raw_from_tc_mode! + end + + macro raw_from_tc_mode! + LibC.cfmakeraw(pointerof(mode)) + LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) end - before = mode - begin - yield mode - ensure - LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(before)) + + private def preserving_tc_mode(msg) + if LibC.tcgetattr(fd, out mode) != 0 + raise IO::Error.from_errno msg + end + before = mode + begin + yield mode + ensure + LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(before)) + end end end -end +{% end %} diff --git a/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr index 90693df681ce..b5e7ac1ebf42 100644 --- a/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/consoleapi.cr @@ -1,6 +1,11 @@ require "c/winnt" lib LibC + ENABLE_PROCESSED_INPUT = 0x0001 + ENABLE_LINE_INPUT = 0x0002 + ENABLE_ECHO_INPUT = 0x0004 + ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 fun GetConsoleMode(hConsoleHandle : HANDLE, lpMode : DWORD*) : BOOL