TTY on Windows

I’m trying to wrap my head around the difference between TTY detection for #tty? (recently edited in Implement `IO#tty?` in Win32 by HertzDevil · Pull Request #14421 · crystal-lang/crystal · GitHub) and ConsoleUtils.console? (used for #unbuffered_read).

Both _isatty and GetFileType(windows_handle) == LibC::FILE_TYPE_CHAR check if the file is a character device.
console? uses GetConsoleMode (and assumes with a non-error return code that the file is a console).

I’m not sure what the exact differences in semantics are. I presume the console mode check is relevant for buffering behaviour respective whether reads are expected to return immedately. And I suppose a character device on Windows alwasy means it’s a tty (or is it just the closes we get?).

/cc @HertzDevil I hope you can shed some light on this.

From what I could gather from c - Detect NUL file descriptor (isatty is bogus) - Stack Overflow, _isatty checks for a character device despite its name, GetConsoleMode checks for a terminal, and on Windows the latter is more specific than the former…? They would probably make a difference for C file descriptors in general, which we are now going to avoid.

Wine’s implementation doesn’t tell a lot: _isatty, GetFileType

1 Like

Actually if you scroll down a bit, msvcrt_init_io sets WX_TTY if GetFileType returns FILE_TYPE_UNKNOWN. That’s presumably where the difference for NUL comes from; it isn’t a terminal on Win32, but the Microsoft C runtime likely treats it as a character device for consistency with /dev/null

This indeed affects NUL and therefore the behavior of Colorize.on_tty_only!:

# test.cr

{% if flag?(:GetConsoleMode) %}
  module Crystal::System::FileDescriptor
    private def system_tty?
      LibC.GetConsoleMode(windows_handle, out _) != 0
    end
  end
{% end %}

p! STDIN.tty?, STDERR.tty?
> bin\crystal run test.cr
STDIN.tty?  # => true
STDERR.tty? # => true

> bin\crystal run test.cr -DGetConsoleMode
STDIN.tty?  # => true
STDERR.tty? # => true

> bin\crystal run test.cr <nul 2>nul
STDIN.tty?  # => true
STDERR.tty? # => true

> bin\crystal run test.cr -DGetConsoleMode <nul 2>nul
STDIN.tty?  # => false
STDERR.tty? # => false

GetConsoleMode is consistent with /dev/null’s behavior:

$ bin/crystal run test.cr
STDIN.tty?  # => true
STDERR.tty? # => true

$ bin/crystal run test.cr </dev/null 2>/dev/null
STDIN.tty?  # => false
STDERR.tty? # => false