10

I'm writing a .NET Core console application. I wanted to limit console input to a certain number of maximum characters for each input. I have some code that does this by building a string with Console.ReadKey() instead of Console.ReadLine() Everything worked perfectly testing it on Windows. Then, when I deployed to a Raspberry Pi 3 running Raspbian, I quickly encountered all sorts of problems. I remembered that Linux handles line endings differently from Windows, and it seems backspaces are handled differently as well. I changed the way I handled those, going off the ConsoleKey instead of the character, and the newline problem went away, but backspaces only sometimes register. Also, sometimes characters get outputted to the console outside of my input box, even though I set the ReadKey to not output to the console on its own. Am I missing something about how Linux handles console input?

//I replaced my calls to Console.ReadLine() with this. The limit is the
//max number of characters that can be entered in the console.
public static string ReadChars(int limit)
{
    string str = string.Empty; //all the input so far
    int left = Console.CursorLeft; //store cursor position for re-outputting
    int top = Console.CursorTop;
    while (true) //keep checking for key events
    {
        if (Console.KeyAvailable)
        {
            //true to intercept input and not output to console
            //normally. This sometimes fails and outputs anyway.
            ConsoleKeyInfo c = Console.ReadKey(true);
            if (c.Key == ConsoleKey.Enter) //stop input on Enter key
                break;
            if (c.Key == ConsoleKey.Backspace) //remove last char on Backspace
            {
                if (str != "")
                {
                     tr = str.Substring(0, str.Length - 1);
                }
            }
            else if (c.Key != ConsoleKey.Tab && str.Length < limit)
            {
                //don't allow tabs or exceeding the max size
                str += c.KeyChar;
            }
            else
            {
                //ignore tabs and when the limit is exceeded
                continue;
            }
            Console.SetCursorPosition(left, top);
            string padding = ""; //padding clears unused chars in field
            for (int i = 0; i < limit - str.Length; i++)
            {
                padding += " ";
            }
            //output this way instead
            Console.Write(str + padding);
        }
    }
    return str;
}
Lex Li
  • 60,503
  • 9
  • 116
  • 147
tyjkenn
  • 709
  • 10
  • 25
  • It's the *terminal* that handles keystrokes. You won't get any keystrokes that aren't sent to your application in the first place. Newlines have nothing to do with this. .NET (and Core) will use the operating system's settings. Besides, it already recognizes `\n` as a newline in Windows – Panagiotis Kanavos Oct 10 '17 at 13:17
  • Windows recognizes carriage return characters, `\r`, while Linux just uses newline characters, `\n`. I was originally checking for `\r`, which, of course led to problems. I was trying to figure out if there were other differences like that that I'm not accounting for. I thought it was implied that this has to with differences in how the terminal handles keystrokes. My point was just that calls to ReadKey give different results on different machines, even if I make the exact same keystrokes, not doubt due to how the different systems process those keystrokes. – tyjkenn Oct 10 '17 at 14:57
  • 4
    Not so sure how any of this makes sense. On Linux you still press the Enter key, you don't press Ctrl+J to get \n. ReadKey tells you about the key that was pressed, not what character it produces. So as long as you use Key and not KeyChar then there shouldn't be a problem. Maybe you exposed a compatibility problem, this is all pretty new so it isn't unthinkable. And they support about ten different Linux flavors and Raspian isn't one of them. Best to tell them about it, use the [New Issue button](https://github.com/dotnet/coreclr/issues). – Hans Passant Oct 10 '17 at 15:12
  • @tyjkenn Linux uses `\n`. Windows uses `\r\n`. Only Classic Mac OS *used* to use `\r`. None of these is relevant to *keystrokes* though, only files, strings and streams. Did you check the [Key](https://msdn.microsoft.com/en-us/library/system.consolekey(v=vs.110).aspx) instead of KeyChar? Key means Shift, Alt, Enter, Down Arrow, Page Down, A, B, C etc. KeyChar is how this is translated into a character. Many keys have no equivalent character – Panagiotis Kanavos Oct 10 '17 at 15:27
  • Never mind about the newline thing. I fixed that by using Key instead of KeyChar, hence my code. Maybe I should've just left that part out, but I thought it would've explained my code better. The change did *not* fix backspaces, and *that* is the real issue. That and the fact that it is outputting to the console when I told it not to. I was afraid it was a compatibility problem, but would there be a workaround? ReadLine works fine, except that I can't limit the number of allow characters. – tyjkenn Oct 10 '17 at 15:39
  • Like other comments indicated, .NET Core only supports a few Linux flavors officially, https://github.com/dotnet/core/blob/master/release-notes/2.0/2.0-supported-os.md, so unless issues are reported and fixed, you should assume it would work sometimes but not always. – Lex Li Oct 14 '17 at 13:56

2 Answers2

5

I think the fundamental issue is exposed by Stephen Toub's comment in this GitHub issue:

You may be thinking of the fact that we now only disable echo during a ReadKey(intercept: true) call, so in a race between the user typing and you calling ReadKey(intercept: true), the key might be echo'd even when you were hoping it wouldn't be, but you won't lose the keystroke.

Which is cold comfort, but accurate. This is a race that is very hard to win. The core problem is that a Linux terminal works very differently from the Windows console. It operates much more like a teletype did back in the 1970s. You banged away on the keyboard, regardless if the computer was paying any attention to what you type, the teletype just echo-ed what you typed banging it out on paper. Until you press the Enter key, then the computer started cranking away at the text.

Very different from the Windows console, it requires the program to have an active Read call to echo any typed text.

So this is a pretty fundamental mismatch with the console api. It needs an Echo property to give you any hope of doing this correctly. So you can set it to false before you start accepting input and take care of the echo yourself. It is still a race, but at least you have a shot at clearing any pre-typed text.

The only half-decent workaround you have now is to disable echo before you start your program. Requiring you to do all input through your method.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Fortunately for me, running all my input through that method is perfectly fine, so I'd consider this workaround a bit more than "half-decent." Thanks! – tyjkenn Oct 18 '17 at 03:04
3

I tested and found out that Console.ReadKey(true) indeed has some bugs where the key actually gets echoed to the console when the typing is fast or repeat keys are fired. This is something that you don't expect, but why it happens I have no idea.

If you are interested in Debugging it, you can look at the below source code

https://referencesource.microsoft.com/#mscorlib/system/console.cs,1476

I chose to put a workaround for the issue. So there are few issues in your approach. The Left Arrow and Right Arrow keys should be handled or they should not be allowed. I chose the later one by adding below code

if (c.Key == ConsoleKey.LeftArrow || c.Key == ConsoleKey.RightArrow) {
   continue;
}

When you type the characters using below

Console.Write(str + padding);

You basically disturb the cursor position also, which is not correct. So you need to set the cursor position after this using below

Console.CursorLeft = str.Length;

Now comes the part of handling the leaky keys which is probably a .NET bug, I added below code

else
{
    //ignore tabs and when the ilimit is exceeded
    if (Console.CursorLeft > str.Length){

        var delta = Console.CursorLeft - str.Length;
        Console.CursorLeft = str.Length;
        Console.Write(new String(' ',delta));
        Console.CursorLeft = str.Length;
    }
    continue;
}

So we check for any unseen reason something was echoed then erase it. Then stress tested it

$ docker run -it console
Please enter some text:
tarun6686e
You entered: tarun6686e

Below is the final code that I had used

using System;

namespace ConsoleTest
{
    public class Program {
        public static string tr="";
        //I replaced my calls to Console.ReadLine() with this. The limit is the
        //max number of characters that can be entered in the console.
        public static string ReadChars(int limit)
        {
            string str = string.Empty; //all the input so far
            int left = Console.CursorLeft; //store cursor position for re-outputting
            int top = Console.CursorTop;

            while (true) //keep checking for key events
            {
                if (Console.KeyAvailable)
                {
                    //true to intercept input and not output to console
                    //normally. This sometimes fails and outputs anyway.
                    ConsoleKeyInfo c = Console.ReadKey(true);
                    string name = Enum.GetName(typeof(ConsoleKey), c.Key);
                    var key = c.KeyChar;
                    // Console.WriteLine(String.Format("Name={0}, Key={1}, KeyAscii={2}", name, key,(int)key));
                    if (c.Key == ConsoleKey.Enter) //stop input on Enter key
                        {
                            Console.WriteLine();
                            break;
                        }
                    if (c.Key == ConsoleKey.LeftArrow || c.Key == ConsoleKey.RightArrow) {
                        continue;
                    }

                    if (c.Key == ConsoleKey.Backspace) //remove last char on Backspace
                    {
                        if (str != "")
                        {
                            str = str.Substring(0, str.Length - 1);
                        }
                    }
                    else if (c.Key != ConsoleKey.Tab && str.Length < limit)
                    {
                        //don't allow tabs or exceeding the max size
                        str += c.KeyChar;
                    }
                    else
                    {
                        //ignore tabs and when the ilimit is exceeded
                        if (Console.CursorLeft > str.Length){

                            var delta = Console.CursorLeft - str.Length;
                            Console.CursorLeft = str.Length;
                            Console.Write(new String(' ',delta));
                            Console.CursorLeft = str.Length;
                        }
                        continue;
                    }
                    Console.SetCursorPosition(left, top);
                    string padding = ""; //padding clears unused chars in field
                    for (int i = 0; i < limit - str.Length; i++)
                    {
                        padding += " ";
                    }
                    //output this way instead
                    Console.Write(str + padding);
                    Console.CursorLeft = str.Length;
                }
            }
            return str;
        }

        public static void Main(string[] args) {
            Console.WriteLine("Please enter some text: ");
            var text = ReadChars(10);

            Console.WriteLine("You entered: " + text);
        }
    }
}
Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
  • Bad link, you need the Unix-specific flavor of Console in .NETCore. I think it is [this one](https://github.com/dotnet/corefx/blob/master/src/System.Console/src/System/ConsolePal.Unix.cs). Looks pretty innocent. Maybe too innocent :) – Hans Passant Oct 14 '17 at 16:47
  • Thanks for the correction @HansPassant, i didn't realize need to look at the .NET core codebase – Tarun Lalwani Oct 14 '17 at 17:26
  • After messing around with this code, I managed to get this approach to work. However, in another part of my program where I am using keyboard input in another way (to flip through pages), I couldn't catch some of the keys while the entire console was being re-drawn. Ultimately, I had to disable echo. – tyjkenn Oct 18 '17 at 03:08