Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement IO::FileDescriptor's console methods on Windows #12294

Merged
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
1 change: 1 addition & 0 deletions samples/2048.cr
Original file line number Diff line number Diff line change
Expand Up @@ -380,4 +380,5 @@ class Game
end
end

at_exit { STDIN.cooked! }
Game.new.run
12 changes: 9 additions & 3 deletions spec/std/io/io_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
37 changes: 37 additions & 0 deletions src/crystal/system/win32/file_descriptor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
284 changes: 188 additions & 96 deletions src/io/console.cr
Original file line number Diff line number Diff line change
@@ -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 %}
5 changes: 5 additions & 0 deletions src/lib_c/x86_64-windows-msvc/c/consoleapi.cr
Original file line number Diff line number Diff line change
@@ -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
Expand Down