dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.91k stars 4.63k forks source link

`System.Console.ReadKey` is lame on Windows; brilliant on Linux #96490

Open tig opened 8 months ago

tig commented 8 months ago

Description

Terminal.Gui uses System.Console for its "least-common-denominator" console driver (NetDriver).

We call System.Console.ReadKey to process keyboard input.

We've noticed that the key info we get on Windows vs Linux is wildy different. Prior to dotnet7, both behaved the same (badly).

With dotnet7 & 8 Linux is WAY better.

For example, if the user presses "Ctrl+7", on Linux we get the right key info. On Windows we get [ConsoleKeyInfo(Key: Null (None), KeyChar: ␟ (31))].

For "Ctrl+7" we get [ConsoleKeyInfo(Key: Space, W (F8) | Control, KeyChar: ␀ (0))]

For reference, "Ctrl+A" works fine: [ConsoleKeyInfo(Key: A (A) | Control, KeyChar: a (65))]

Is there something we can do to make Windows work as well as Linux (what a strange thing to ask :-))?

Reproduction Steps

Using the v2_develop branch of Terminal.Gui debug the UI Catalog -usc in the VS debugger. This runs UI Catalog using the NetDriver.

Open the Keys scenario and press "Ctrl+7" (you might need to unbind that key from WT's actions).

Note garbage. image

Now run the WSL: UI Catalog -usc debug profile. This runs UI Catalog in WSL using NetDriver.

Open Keys and press "Ctrl+7". You'll see this: image

Expected behavior

image

Actual behavior

image

Regression?

No response

Known Workarounds

No response

Configuration

No response

Other information

No response

ghost commented 8 months ago

Tagging subscribers to this area: @dotnet/area-system-console See info in area-owners.md if you want to be subscribed.

Issue Details
### Description [Terminal.Gu](https://github.com/gui-cs/Terminal.Gui) uses `System.Console` for its "least-common-denominator" console driver (`NetDriver`). We call `System.Console.ReadKey` to process keyboard input. We've noticed that the key info we get on Windows vs Linux is wildy different. Prior to dotnet7, both behaved the same (badly). With dotnet7 & 8 Linux is WAY better. For example, if the user presses "Ctrl+7", on Linux we get the right key info. On Windows we get `[ConsoleKeyInfo(Key: Null (None), KeyChar: ␟ (31))]`. For "Ctrl+7" we get `[ConsoleKeyInfo(Key: Space, W (F8) | Control, KeyChar: ␀ (0))]` For reference, "Ctrl+A" works fine: `[ConsoleKeyInfo(Key: A (A) | Control, KeyChar: a (65))]` Is there something we can do to make Windows work as well as Linux (what a strange thing to ask :-))? ### Reproduction Steps Using the `v2_develop` branch of [Terminal.Gu](https://github.com/gui-cs/Terminal.Gui) debug the `UI Catalog -usc` in the VS debugger. This runs UI Catalog using the `NetDriver`. Open the `Keys` scenario and press "Ctrl+7" (you might need to unbind that key from WT's actions). Note garbage. ![image](https://github.com/dotnet/runtime/assets/585482/8d7755f9-2a5f-4f02-b7de-d6fc4629a0a5) Now run the `WSL: UI Catalog -usc` debug profile. This runs UI Catalog in WSL using NetDriver. Open `Keys` and press "Ctrl+7". You'll see this: ![image](https://github.com/dotnet/runtime/assets/585482/32432714-c24a-42bd-b0ec-e17c9f2deb9e) ### Expected behavior ![image](https://github.com/dotnet/runtime/assets/585482/7620d615-6733-47f5-9631-1f1fb201133b) ### Actual behavior ![image](https://github.com/dotnet/runtime/assets/585482/8d7755f9-2a5f-4f02-b7de-d6fc4629a0a5) ### Regression? _No response_ ### Known Workarounds _No response_ ### Configuration _No response_ ### Other information _No response_
Author: tig
Assignees: -
Labels: `area-System.Console`, `untriaged`
Milestone: -
adamsitnik commented 8 months ago

Hi @tig

With dotnet7 & 8 Linux is WAY better

I am happy to hear that you like the improvements we made in https://devblogs.microsoft.com/dotnet/console-readkey-improvements-in-net-7/

For example, if the user presses "Ctrl+7", on Linux we get the right key info. On Windows we get [ConsoleKeyInfo(Key: Null (None), KeyChar: ␟ (31))].

Do you apply any custom settings related to Console/Terminal on Windows? The following app works just fine:

namespace ReadKey96490
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Press any key to get info or Escape to stop");

            ConsoleKeyInfo consoleKeyInfo;
            do
            {
                consoleKeyInfo = Console.ReadKey();
                Console.WriteLine($"{consoleKeyInfo.Key} {consoleKeyInfo.Modifiers} {consoleKeyInfo.KeyChar}");
            } while (consoleKeyInfo.Key != ConsoleKey.Escape);
        }
    }
}

image

tig commented 8 months ago

Yes, on Windows we enable ENABLE_VIRTUAL_TERMINAL_INPUT via SetConsoleMode (P/Invoke).

I just tested this and if we disable setting ENABLE_VIRTUAL_TERMINAL_INPUT on Windows, keyboard behavior is like WSL. Yay.

However, mouse input no longer works when we do this, even if ENABLE_EXTENDED_FLAGS and ENABLE_MOUSE_INPUT are enabled (https://stackoverflow.com/questions/42213161/console-mouse-input-not-working).

On WSL, we see esc codes for mouse come in via ReadKey and we process those appropriately. But without setting ENABLE_VIRTUAL_TERMINAL_INPUT on Windows we don't see those esc codes.

What are we missing?

FWIW, here's the current setup code:

_inputHandle = GetStdHandle (STD_INPUT_HANDLE);
if (!GetConsoleMode (_inputHandle, out var mode)) {
    throw new ApplicationException ($"Failed to get input console mode, error code: {GetLastError ()}.");
}
_originalInputConsoleMode = mode;
if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT) {
    mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
    if (!SetConsoleMode (_inputHandle, mode)) {
        throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}.");
    }
}

mode |= ENABLE_EXTENDED_FLAGS | ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT;
if (!SetConsoleMode (_inputHandle, mode)) {
    throw new ApplicationException ($"Failed to set input console mode, error code: {GetLastError ()}.");
}

_outputHandle = GetStdHandle (STD_OUTPUT_HANDLE);
if (!GetConsoleMode (_outputHandle, out mode)) {
    throw new ApplicationException ($"Failed to get output console mode, error code: {GetLastError ()}.");
}
_originalOutputConsoleMode = mode;
if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN) {
    mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
    if (!SetConsoleMode (_outputHandle, mode)) {
        throw new ApplicationException ($"Failed to set output console mode, error code: {GetLastError ()}.");
    }
}

_errorHandle = GetStdHandle (STD_ERROR_HANDLE);
if (!GetConsoleMode (_errorHandle, out mode)) {
    throw new ApplicationException ($"Failed to get error console mode, error code: {GetLastError ()}.");
}
_originalErrorConsoleMode = mode;
if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN) {
    mode |= DISABLE_NEWLINE_AUTO_RETURN;
    if (!SetConsoleMode (_errorHandle, mode)) {
        throw new ApplicationException ($"Failed to set error console mode, error code: {GetLastError ()}.");
    }
}

If I remove the code that enables ENABLE_VIRTUAL_TERMINAL_INPUT keyboard input works great. But we don't get mouse events.

tig commented 8 months ago

Full standalone test app:

using System.Runtime.InteropServices;
using System.Text;

namespace ReadKey96490;

internal class Program
{
    static void Main(string[] args)
    {
        var console = new NetWinVTConsole();

        console.PrintModes();

        Console.WriteLine("Press any key to get info or Enter to stop.");

        ConsoleKeyInfo consoleKeyInfo;
        do
        {
            consoleKeyInfo = Console.ReadKey();
            consoleKeyInfo.Print();
        } while (consoleKeyInfo.Key != ConsoleKey.Enter);

        console?.Cleanup();
    }
}

public static class ConsoleKeyInfoExtensions
{
    public static void Print (this ConsoleKeyInfo cki)
    {
        var sb = new StringBuilder();
        sb.Append($"Key: {cki.Key} ({(int)cki.Key})");
        sb.Append((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
        sb.Append((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
        sb.Append((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
        var s = sb.ToString().TrimEnd(',').TrimEnd(' ');
        Console.WriteLine($" [ConsoleKeyInfo({s})]");
    }
}

/// <summary>
/// On Windows, System.Console does not send mouse input via Console.ReadKey. This class
/// provides PInvoke support for enabling ENABLE_VIRTUAL_TERMINAL_INPUT which causee
/// System.Console to send mouse input.
///
/// BUGBUG: https://github.com/dotnet/runtime/issues/96490
/// 
/// </summary>
class NetWinVTConsole
{

    const int STD_INPUT_HANDLE = -10;
    const int STD_OUTPUT_HANDLE = -11;
    const int STD_ERROR_HANDLE = -12;

    // Input modes: https://learn.microsoft.com/en-us/windows/console/setconsolemode
    /// <summary>
    /// CTRL+C is processed by the system and is not placed in the input buffer. If the input buffer is
    /// being read by ReadFile or ReadConsole, other control keys are processed by the system and are not
    /// returned in the ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also enabled,
    /// backspace, carriage return, and line feed characters are handled by the system.
    /// </summary>
    const uint ENABLE_PROCESSED_INPUT = 1;

    /// <summary>
    /// The ReadFile or ReadConsole function returns only when a carriage return character is read. If this
    /// mode is disabled, the functions return when one or more characters are available.
    /// </summary>
    const uint ENABLE_LINE_INPUT = 2;

    /// <summary>
    /// Characters read by the ReadFile or ReadConsole function are written to the active screen buffer as
    /// they are typed into the console. This mode can be used only if the ENABLE_LINE_INPUT mode is also
    /// enabled.
    /// </summary>
    const uint ENABLE_ECHO_INPUT = 4;

    /// <summary>
    /// User interactions that change the size of the console screen buffer are reported in the console's
    /// input buffer. Information about these events can be read from the input buffer by applications
    /// using the ReadConsoleInput function, but not by those using ReadFile or ReadConsole.
    /// </summary>
    const uint ENABLE_WINDOW_INPUT = 8;

    /// <summary>
    /// If the mouse pointer is within the borders of the console window and the window has the keyboard
    /// focus, mouse events generated by mouse movement and button presses are placed in the input buffer.
    /// These events are discarded by ReadFile or ReadConsole, even when this mode is enabled. The
    /// ReadConsoleInput function can be used to read MOUSE_EVENT input records from the input buffer.
    /// </summary>
    const uint ENABLE_MOUSE_INPUT = 16;

    /// <summary>
    /// When enabled, text entered in a console window will be inserted at the current cursor location and
    /// all text following that location will not be overwritten. When disabled, all following text will be
    /// overwritten.
    /// To enable this mode, use ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS.
    /// To disable this mode, use ENABLE_EXTENDED_FLAGS without this flag.
    /// </summary>
    const uint ENABLE_INSERT_MODE = 32;

    /// <summary>
    /// This flag enables the user to use the mouse to select and edit text. To enable this mode, use
    /// ENABLE_QUICK_EDIT_MODE | ENABLE_EXTENDED_FLAGS. To disable this mode, use ENABLE_EXTENDED_FLAGS
    /// without this flag.
    /// </summary>
    const uint ENABLE_QUICK_EDIT_MODE = 64;

    /// <summary>
    /// Required to enable or disable extended flags.
    /// See ENABLE_INSERT_MODE and ENABLE_QUICK_EDIT_MODE.
    /// </summary>
    const uint ENABLE_EXTENDED_FLAGS = 128;

    /// <summary>
    /// Setting this flag directs the Virtual Terminal processing engine to convert user input received by
    /// the console window into Console Virtual Terminal Sequences that can be retrieved by a supporting
    /// application through ReadFile or ReadConsole functions.
    /// The typical usage of this flag is intended in conjunction with ENABLE_VIRTUAL_TERMINAL_PROCESSING
    /// on the output handle to connect to an application that communicates exclusively via virtual
    /// terminal sequences.
    /// </summary>
    const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 512;

    // Output modes.
    /// <summary>
    /// Characters written by the WriteFile or WriteConsole function or echoed by the ReadFile or
    /// ReadConsole function are parsed for ASCII control sequences, and the correct action is performed.
    /// Backspace, tab, bell, carriage return, and line feed characters are processed. It should be enabled
    /// when using control sequences or when ENABLE_VIRTUAL_TERMINAL_PROCESSING is set.
    /// </summary>
    const uint ENABLE_PROCESSED_OUTPUT = 1;

    /// <summary>
    ///     When writing with WriteFile or WriteConsole or echoing with ReadFile or ReadConsole, the cursor
    /// moves to the beginning of the next row when it reaches the end of the current row. This causes the
    /// rows displayed in the console window to scroll up automatically when the cursor advances beyond the
    /// last row in the window. It also causes the contents of the console screen buffer to scroll up
    /// (../discarding the top row of the console screen buffer) when the cursor advances beyond the last
    /// row in the console screen buffer. If this mode is disabled, the last character in the row is
    /// overwritten with any subsequent characters.
    /// </summary>
    const uint ENABLE_WRAP_AT_EOL_OUTPUT = 2;

    /// <summary>
    /// When writing with WriteFile or WriteConsole, characters are parsed for VT100 and similar control
    /// character sequences that control cursor movement, color/font mode, and other operations that can
    /// also be performed via the existing Console APIs. For more information, see Console Virtual Terminal
    /// Sequences.
    /// Ensure ENABLE_PROCESSED_OUTPUT is set when using this flag.
    /// </summary>
    const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4;

    /// <summary>
    /// When writing with WriteFile or WriteConsole, this adds an additional state to end-of-line wrapping
    /// that can delay the cursor move and buffer scroll operations.
    /// Normally when ENABLE_WRAP_AT_EOL_OUTPUT is set and text reaches the end of the line, the cursor
    /// will immediately move to the next line and the contents of the buffer will scroll up by one line.In
    /// contrast with this flag set, the cursor does not move to the next line, and the scroll operation is
    /// not performed. The written character will be printed in the final position on the line and the
    /// cursor will remain above this character as if ENABLE_WRAP_AT_EOL_OUTPUT was off, but the next
    /// printable character will be printed as if ENABLE_WRAP_AT_EOL_OUTPUT is on.No overwrite will occur.
    /// Specifically, the cursor quickly advances down to the following line, a scroll is performed if
    /// necessary, the character is printed, and the cursor advances one more position.
    /// The typical usage of this flag is intended in conjunction with setting
    /// ENABLE_VIRTUAL_TERMINAL_PROCESSING to better emulate a terminal emulator where writing the final
    /// character on the screen (../in the bottom right corner) without triggering an immediate scroll is
    /// the desired behavior.
    /// </summary>
    const uint DISABLE_NEWLINE_AUTO_RETURN = 8;

    /// <summary>
    /// The APIs for writing character attributes including WriteConsoleOutput and
    /// WriteConsoleOutputAttribute allow the usage of flags from character attributes to adjust the color
    /// of the foreground and background of text. Additionally, a range of DBCS flags was specified with
    /// the COMMON_LVB prefix. Historically, these flags only functioned in DBCS code pages for Chinese,
    /// Japanese, and Korean languages.
    /// With exception of the leading byte and trailing byte flags, the remaining flags describing line
    /// drawing and reverse video (../swap foreground and background colors) can be useful for other
    /// languages to emphasize portions of output.
    /// Setting this console mode flag will allow these attributes to be used in every code page on every
    /// language.
    /// It is off by default to maintain compatibility with known applications that have historically taken
    /// advantage of the console ignoring these flags on non-CJK machines to store bits in these fields for
    /// their own purposes or by accident.
    /// Note that using the ENABLE_VIRTUAL_TERMINAL_PROCESSING mode can result in LVB grid and reverse
    /// video flags being set while this flag is still off if the attached application requests underlining
    /// or inverse video via Console Virtual Terminal Sequences.
    /// </summary>
    const uint ENABLE_LVB_GRID_WORLDWIDE = 10;

    readonly IntPtr _errorHandle;
    readonly IntPtr _inputHandle;
    readonly uint _originalErrorConsoleMode;
    readonly uint _originalInputConsoleMode;
    readonly uint _originalOutputConsoleMode;
    readonly IntPtr _outputHandle;

    [DllImport("kernel32.dll", SetLastError = true)]
    extern static IntPtr GetStdHandle(int nStdHandle);

    [DllImport("kernel32.dll")]
    extern static bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);

    [DllImport("kernel32.dll")]
    extern static bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);

    [DllImport("kernel32.dll")]
    extern static uint GetLastError();

    public NetWinVTConsole()
    {
        _inputHandle = GetStdHandle(STD_INPUT_HANDLE);
        if (!GetConsoleMode(_inputHandle, out var mode))
        {
            throw new ApplicationException($"Failed to get input console mode, error code: {GetLastError()}.");
        }
        _originalInputConsoleMode = mode;

        mode = ENABLE_EXTENDED_FLAGS;
        if (!SetConsoleMode(_inputHandle, mode))
        {
            throw new ApplicationException($"Failed to set input console mode, error code: {GetLastError()}.");
        }

#if SET_ENABLE_VIRTUAL_TERMINAL_INPUT
        if ((mode & ENABLE_VIRTUAL_TERMINAL_INPUT) < ENABLE_VIRTUAL_TERMINAL_INPUT)
        {
            mode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
            if (!SetConsoleMode(_inputHandle, mode))
            {
                throw new ApplicationException($"Failed to set input console mode, error code: {GetLastError()}.");
            }
        }
#endif        

        mode |= ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT;
        if (!SetConsoleMode(_inputHandle, mode))
        {
            throw new ApplicationException($"Failed to set input console mode, error code: {GetLastError()}.");
        }

        _outputHandle = GetStdHandle(STD_OUTPUT_HANDLE);
        if (!GetConsoleMode(_outputHandle, out mode))
        {
            throw new ApplicationException($"Failed to get output console mode, error code: {GetLastError()}.");
        }

        _originalOutputConsoleMode = mode;
        if ((mode & (ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) < DISABLE_NEWLINE_AUTO_RETURN)
        {
            mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN;
            if (!SetConsoleMode(_outputHandle, mode))
            {
                throw new ApplicationException($"Failed to set output console mode, error code: {GetLastError()}.");
            }
        }

        _errorHandle = GetStdHandle(STD_ERROR_HANDLE);
        if (!GetConsoleMode(_errorHandle, out mode))
        {
            throw new ApplicationException($"Failed to get error console mode, error code: {GetLastError()}.");
        }
        _originalErrorConsoleMode = mode;
        if ((mode & DISABLE_NEWLINE_AUTO_RETURN) < DISABLE_NEWLINE_AUTO_RETURN)
        {
            mode |= DISABLE_NEWLINE_AUTO_RETURN;
            if (!SetConsoleMode(_errorHandle, mode))
            {
                throw new ApplicationException($"Failed to set error console mode, error code: {GetLastError()}.");
            }
        }
    }

    public void PrintModes() {
        if (!GetConsoleMode(_inputHandle, out var mode))
        {
            throw new ApplicationException($"Failed to get input console mode, error code: {GetLastError()}.");
        }
        Console.WriteLine($"Input mode: {PrettyPrintConsoleModes(mode)}");
        if (!GetConsoleMode(_outputHandle, out mode))
        {
            throw new ApplicationException($"Failed to get output console mode, error code: {GetLastError()}.");
        }
        Console.WriteLine($"Output mode: {PrettyPrintConsoleModes(mode)}");
        if (!GetConsoleMode(_errorHandle, out mode))
        {
            throw new ApplicationException($"Failed to get error console mode, error code: {GetLastError()}.");
        }
        Console.WriteLine($"Error mode: {PrettyPrintConsoleModes(mode)}");

    }

    public static string PrettyPrintConsoleModes(uint mode)
    {
        var sb = new StringBuilder();

        AppendFlag(sb, mode, ENABLE_PROCESSED_INPUT, "ENABLE_PROCESSED_INPUT");
        AppendFlag(sb, mode, ENABLE_LINE_INPUT, "ENABLE_LINE_INPUT");
        AppendFlag(sb, mode, ENABLE_ECHO_INPUT, "ENABLE_ECHO_INPUT");
        AppendFlag(sb, mode, ENABLE_WINDOW_INPUT, "ENABLE_WINDOW_INPUT");
        AppendFlag(sb, mode, ENABLE_MOUSE_INPUT, "ENABLE_MOUSE_INPUT");
        AppendFlag(sb, mode, ENABLE_INSERT_MODE, "ENABLE_INSERT_MODE");
        AppendFlag(sb, mode, ENABLE_QUICK_EDIT_MODE, "ENABLE_QUICK_EDIT_MODE");
        AppendFlag(sb, mode, ENABLE_EXTENDED_FLAGS, "ENABLE_EXTENDED_FLAGS");
        AppendFlag(sb, mode, ENABLE_VIRTUAL_TERMINAL_INPUT, "ENABLE_VIRTUAL_TERMINAL_INPUT");
        AppendFlag(sb, mode, ENABLE_PROCESSED_OUTPUT, "ENABLE_PROCESSED_OUTPUT");
        AppendFlag(sb, mode, ENABLE_WRAP_AT_EOL_OUTPUT, "ENABLE_WRAP_AT_EOL_OUTPUT");
        AppendFlag(sb, mode, ENABLE_VIRTUAL_TERMINAL_PROCESSING, "ENABLE_VIRTUAL_TERMINAL_PROCESSING");
        AppendFlag(sb, mode, DISABLE_NEWLINE_AUTO_RETURN, "DISABLE_NEWLINE_AUTO_RETURN");
        AppendFlag(sb, mode, ENABLE_LVB_GRID_WORLDWIDE, "ENABLE_LVB_GRID_WORLDWIDE");

        return sb.ToString().TrimEnd('|', ' ');
    }

    private static void AppendFlag(StringBuilder sb, uint mode, uint flag, string flagName)
    {
        if ((mode & flag) == flag)
        {
            sb.Append(flagName).Append(" | ");
        }
    }

    public void Cleanup()
    {
        if (!SetConsoleMode(_inputHandle, _originalInputConsoleMode))
        {
            throw new ApplicationException($"Failed to restore input console mode, error code: {GetLastError()}.");
        }
        if (!SetConsoleMode(_outputHandle, _originalOutputConsoleMode))
        {
            throw new ApplicationException($"Failed to restore output console mode, error code: {GetLastError()}.");
        }
        if (!SetConsoleMode(_errorHandle, _originalErrorConsoleMode))
        {
            throw new ApplicationException($"Failed to restore error console mode, error code: {GetLastError()}.");
        }
    }
}

Without ENABLE_VIRTUAL_TERMINAL_INPUT (pressed Ctrl+7, moved mouse, ENTER):

dotnet run 
Input mode: ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS | DISABLE_NEWLINE_AUTO_RETURN
Output mode: ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_WINDOW_INPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN | ENABLE_LVB_GRID_WORLDWIDE
Error mode: ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_WINDOW_INPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN | ENABLE_LVB_GRID_WORLDWIDE
Press any key to get info or Enter to stop.
 [ConsoleKeyInfo(Key: D7 (55) | Control)]
 [ConsoleKeyInfo(Key: Enter (13))]

With ENABLE_VIRTUAL_TERMINAL_INPUT (pressed Ctrl+7, moved mouse, Enter, Ctrl-Break):

dotnet run --property:DefineConstants=SET_ENABLE_VIRTUAL_TERMINAL_INPUT
Input mode: ENABLE_WINDOW_INPUT | ENABLE_MOUSE_INPUT | ENABLE_EXTENDED_FLAGS | ENABLE_VIRTUAL_TERMINAL_INPUT | DISABLE_NEWLINE_AUTO_RETURN
Output mode: ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_WINDOW_INPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN | ENABLE_LVB_GRID_WORLDWIDE
Error mode: ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_WINDOW_INPUT | ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN | ENABLE_LVB_GRID_WORLDWIDE
Press any key to get info or Enter to stop.
 [ConsoleKeyInfo(Key: None (0))]
 [ConsoleKeyInfo(Key: None (0))]
[ [ConsoleKeyInfo(Key: None (0))]
< [ConsoleKeyInfo(Key: None (0))]
3 [ConsoleKeyInfo(Key: None (0))]
5 [ConsoleKeyInfo(Key: None (0))]
; [ConsoleKeyInfo(Key: None (0))]
8 [ConsoleKeyInfo(Key: None (0))]
2 [ConsoleKeyInfo(Key: None (0))]
; [ConsoleKeyInfo(Key: None (0))]
3 [ConsoleKeyInfo(Key: None (0))]
5 [ConsoleKeyInfo(Key: None (0))]
m [ConsoleKeyInfo(Key: None (0))]
adamsitnik commented 7 months ago

@tig big thanks for providing a very detailed information!

PowerShell has requested for the same thing in the past: https://github.com/dotnet/runtime/issues/60107

Would addressing this completely unblock your scenario? Are there any other Console APIs that are not working correctly for ENABLE_VIRTUAL_TERMINAL_INPUT?

tig commented 6 months ago

@tig big thanks for providing a very detailed information!

PowerShell has requested for the same thing in the past: #60107

Would addressing this completely unblock your scenario? Are there any other Console APIs that are not working correctly for ENABLE_VIRTUAL_TERMINAL_INPUT?

Yes, mouse input appears to be horked as well. I have not had time to dive into it deeply, but we're not getting all "clicked" events (we do get pressed/released) and SOMETIMEs "clicked".

matteocoder commented 2 months ago

Another issue related to ReadKey() concerns dead keys: on Windows they are completely broken. As you can see in https://github.com/PowerShell/PSReadLine/issues/3795, searching for accented characters using the PSReadline functions is impossible because ReadKey() returns before the accented character is fully composed.

@thomazmoura researched the possible causes, an here is what he found:

Just to give an update I've so far found that the code which is likely the culprit is the SearchChar method on ReadLine.vi.cs (line 166). There it calls ReadKey() and after getting the value proceeds to make the movement. The part where key is actually read is not shown on the ReadKey() method (it seems to happen on another thread) but my guess is that it runs something similar to calling [Console]::ReadKey() on pwsh and that's where Windows and Linux diverge considerably. On Linux, any dead keys are ignored by [Console]::ReadKey() - only when the next character is typed it returns the combined character (like á, or ã) that way on layouts that have ', " or ` as dead keys it waits for a another key (like space) to return the key. On windows though, this same method returns immediately when a dead key is typed, but with a null/empty KeyChar. The PSReadLine reads this empty KeyChar and then does nothing. When the user finally types the inteded key the chain has already been broken by the dead key.