10

Is there any way to detect both Readline and ReadKey, so that in most cases it behaves as a readline, except for some special key inputs that should be detected?

I need some "parallel" implementation to introduce simultaneity. The code below is synchronous and does not meet my need

while ((line = Console.ReadLine()) != "x")
{    
    if (line == "BLABLA")
    {
        //Stuff
    }

    else
    {
        //Stuff
    }

    ConsoleKeyInfo ki = Console.ReadKey(true);
    if ((ki.Key == ConsoleKey.V) && (ki.Modifiers == ConsoleModifiers.Control))
    {
        //Stuff
    }
}
Jeff LaFay
  • 12,882
  • 13
  • 71
  • 101
Mehdi LAMRANI
  • 11,289
  • 14
  • 88
  • 130
  • possible duplicate http://stackoverflow.com/questions/5891538/c-sharp-listen-for-key-press-in-console-app – Alex Jan 09 '12 at 14:45

4 Answers4

6

Here's a function I just made to do this.

Right now it only handles Backspace, Enter and Esc, but it could easily be modified to handle other keys if you deem them necessary.

    // returns null if user pressed Escape, or the contents of the line if they pressed Enter.
    private static string ReadLineOrEsc()
    {
        string retString = "";

        int curIndex = 0;
        do
        {
            ConsoleKeyInfo readKeyResult = Console.ReadKey(true);

            // handle Esc
            if (readKeyResult.Key == ConsoleKey.Escape)
            {
                Console.WriteLine();
                return null;
            }

            // handle Enter
            if (readKeyResult.Key == ConsoleKey.Enter)
            {
                Console.WriteLine();
                return retString;
            }

            // handle backspace
            if (readKeyResult.Key == ConsoleKey.Backspace)
            {
                if (curIndex > 0)
                {
                    retString = retString.Remove(retString.Length - 1);
                    Console.Write(readKeyResult.KeyChar);
                    Console.Write(' ');
                    Console.Write(readKeyResult.KeyChar);
                    curIndex--;
                }
            }
            else
            // handle all other keypresses
            {
                retString += readKeyResult.KeyChar;
                Console.Write(readKeyResult.KeyChar);
                curIndex++;
            }
        }
        while (true);
    }
Jackdaw
  • 7,626
  • 5
  • 15
  • 33
Overlord Zurg
  • 3,430
  • 2
  • 22
  • 27
  • With four alternative courses of action based on the key, I'd go for a `switch` statement instead of three `if`'s and an `else`. – Kristoffer Lerbæk Pedersen Jan 30 '19 at 20:54
  • The backspace handling didn't work for me on linux, but replacing the backspace logic with `Console.Write("\b \b")` did work (from https://stackoverflow.com/a/64078443/3525158) – chanban Aug 29 '23 at 19:36
  • Also, can be improved by using a `StringBuilder` instead of tracking `curIndex` and creating copies of `retString` – chanban Aug 29 '23 at 19:43
2

No, not as such. Both methods block until the user enters something on the console. So even if you would find a way to have both run in parallel, it will not be deterministic which one gets the first shot.

There is a (not obvious) similar problem: how to make Console.ReadLine() abort/break after a certain amount of time without user input.

There have been multiple attempts for this problem here:

Most are modelled around either creating your own version of a ReadLine function that adds a timeout (or in your case special handling for certain character (codes)) or the use some sort of threading.

Both ways are either non-trivial or have their own issues (make sure you review the comments, even for the accepted answers).

In short, I think you will need to roll your own version of ReadLine, based on Console.ReadKey with your special handling included and that much of the genuine Console.ReadLine behavior that you require. Note that this even include such basic things as RETURN, ARROW KEYS, BACKSPACE handling, etc.

Update: There is the getline.cs Code from the Mono project, which implements a line editing capability like it was provided by some venerable UNIX shells (EMACS mode, if you care). For that, I believe it will need to implement some sort of ReadLine replacement, although I haven't checked. Maybe you can use this as a starting point.

Community
  • 1
  • 1
Christian.K
  • 47,778
  • 10
  • 99
  • 143
  • 1
    Definitely not worth it to implement a whole comprehensive readline logic. Too much a hassle for such a "small" feature. Can't believe it's not possible more simply, as it's a feature one would really expect from a shell. Besides, I really did not get the point : What does aborting readline after a certain amount of time has to do with my post ? – Mehdi LAMRANI Jan 09 '12 at 14:39
  • @MikaJacobi It also requires you to tailor ReadLine in a way it wasn't initially ment for. In particular, at least some implementations also require a rewrite, which you could extend with your functionality. – Christian.K Jan 09 '12 at 14:40
  • @MehdiLAMRANI, I know this question is 8 years old, but it seems like Overlord Zurg's solution does what you were looking for and what Christian.K describes with very little difficulty. – Lou Apr 29 '20 at 08:29
1

Here is a method that I created that works great. You do not have to press button twice in order for string to start appearing. Basically this replaces Console.ReadLine() but it also looks for Esc key pressed. Just look at return type of method if it is null then you know Esc was pressed.

private string ReadLineOrEscape()
{
    ConsoleKeyInfo keyInfo = new ConsoleKeyInfo();
    StringBuilder sb = new StringBuilder();
    int index = 0;

    while (keyInfo.Key != ConsoleKey.Enter)
    {
        keyInfo = Console.ReadKey(true);

        if (keyInfo.Key == ConsoleKey.Escape)
        {
            return null;
        }

        if(keyInfo.Key == ConsoleKey.Backspace)
        {
            if (index > 0)
            {
                Console.CursorLeft = index - 1;

                sb.Remove(index - 1, 1);

                Console.Write(" \b");

                index--;
            }
            
        }

        if(keyInfo.KeyChar > 31 && keyInfo.KeyChar < 127)
        {
            index++;
            Console.Write(keyInfo.KeyChar);
            sb.Append(keyInfo.KeyChar);

        }

        
    }
    return sb.ToString(); ;
}
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Sillycrackers
  • 31
  • 1
  • 6
0

In response to @Overlord Zurd, I improved the code provided by the user.

public class ConsoleOutput
{
    private ConsoleOutputType OutputType { get; set; }
    private object MyObject { get; }

    private static bool IsInserting { get; set; }
    public string KeyName => IsKey() && (ConsoleKeyInfo)MyObject != null ? ((ConsoleKeyInfo)MyObject).Key.ToString() : "Null";
    public string OutputString => !IsKey() && MyObject != null ? (string)MyObject : string.Empty;

    public static event Action<string> ReadInput = delegate { };

    public static event Action<ConsoleKeyInfo> ReadKey = delegate { };

    private ConsoleOutput()
    {
    }

    public ConsoleOutput(object obj)
    {
        MyObject = obj;

        OutputType = obj is ConsoleKeyInfo ? ConsoleOutputType.Key : ConsoleOutputType.Value;
    }

    public bool IsKey()
    {
        return OutputType == ConsoleOutputType.Key;
    }

    public bool IsExitKey()
    {
        if (!IsKey())
            return false;

        var info = ((ConsoleKeyInfo)MyObject);
        return (info.Modifiers & ConsoleModifiers.Control) != 0 && info.Key == ConsoleKey.B;
    }

    public string GetValue()
    {
        return (string)MyObject;
    }

    // returns null if user pressed Escape, or the contents of the line if they pressed Enter.
    public static ConsoleOutput ReadLineOrKey()
    {
        string retString = "";

        int curIndex = 0;
        do
        {
            ConsoleKeyInfo readKeyResult = Console.ReadKey(true);

            // handle Enter
            if (readKeyResult.Key == ConsoleKey.Enter)
            {
                ReadInput?.Invoke(retString);

                Console.WriteLine();
                return new ConsoleOutput(retString);
            }

            // handle backspace
            if (readKeyResult.Key == ConsoleKey.Backspace)
            {
                if (curIndex > 0)
                {
                    retString = retString.Remove(retString.Length - 1);

                    Console.Write(readKeyResult.KeyChar);
                    Console.Write(' ');
                    Console.Write(readKeyResult.KeyChar);

                    --curIndex;
                }
            }
            else if (readKeyResult.Key == ConsoleKey.Delete)
            {
                if (retString.Length - curIndex > 0)
                {
                    // Store current position
                    int curLeftPos = Console.CursorLeft;

                    // Redraw string
                    for (int i = curIndex + 1; i < retString.Length; ++i)
                        Console.Write(retString[i]);

                    // Remove last repeated char
                    Console.Write(' ');

                    // Restore position
                    Console.SetCursorPosition(curLeftPos, Console.CursorTop);

                    // Remove string
                    retString = retString.Remove(curIndex, 1);
                }
            }
            else if (readKeyResult.Key == ConsoleKey.RightArrow)
            {
                if (curIndex < retString.Length)
                {
                    ++Console.CursorLeft;
                    ++curIndex;
                }
            }
            else if (readKeyResult.Key == ConsoleKey.LeftArrow)
            {
                if (curIndex > 0)
                {
                    --Console.CursorLeft;
                    --curIndex;
                }
            }
            else if (readKeyResult.Key == ConsoleKey.Insert)
            {
                IsInserting = !IsInserting;
            }
#if DEBUG
            else if (readKeyResult.Key == ConsoleKey.UpArrow)
            {
                if (Console.CursorTop > 0)
                    --Console.CursorTop;
            }
            else if (readKeyResult.Key == ConsoleKey.DownArrow)
            {
                if (Console.CursorTop < Console.BufferHeight - 1)
                    ++Console.CursorTop;
            }
#endif
            else
            // handle all other keypresses
            {
                if (IsInserting || curIndex == retString.Length)
                {
                    retString += readKeyResult.KeyChar;
                    Console.Write(readKeyResult.KeyChar);
                    ++curIndex;
                }
                else
                {
                    // Store char
                    char c = readKeyResult.KeyChar;

                    // Write char at position
                    Console.Write(c);

                    // Store cursor position
                    int curLeftPos = Console.CursorLeft;

                    // Clear console from curIndex to end
                    for (int i = curIndex; i < retString.Length; ++i)
                        Console.Write(' ');

                    // Go back
                    Console.SetCursorPosition(curLeftPos, Console.CursorTop);

                    // Write the chars from curIndex to end (with the new appended char)
                    for (int i = curIndex; i < retString.Length; ++i)
                        Console.Write(retString[i]);

                    // Restore again
                    Console.SetCursorPosition(curLeftPos, Console.CursorTop);

                    // Store in the string
                    retString = retString.Insert(curIndex, new string(c, 1));

                    // Sum one to the cur index (we appended one char)
                    ++curIndex;
                }
            }

            if (char.IsControl(readKeyResult.KeyChar) &&
                readKeyResult.Key != ConsoleKey.Enter &&
                readKeyResult.Key != ConsoleKey.Backspace &&
                readKeyResult.Key != ConsoleKey.Tab &&
                readKeyResult.Key != ConsoleKey.Delete &&
                readKeyResult.Key != ConsoleKey.RightArrow &&
                readKeyResult.Key != ConsoleKey.LeftArrow &&
                readKeyResult.Key != ConsoleKey.Insert)
            {
#if DEBUG
                if (readKeyResult.Key == ConsoleKey.UpArrow || readKeyResult.Key == ConsoleKey.DownArrow)
                    continue;
#endif

                ReadKey?.Invoke(readKeyResult);

                Console.WriteLine();
                return new ConsoleOutput(readKeyResult);
            }
        }
        while (true);
    }
}

As you can see, I implemented Insert, Arrow control, Delete, etc etc... (Insert was an important thing because if you write any text with this code you will see behavior as Insert key provides).

And example of use:

internal class Program
{
    private static void Main(string[] args)
    {
        Console.Write("Write test string: ");
        var test = ConsoleOutput.ReadLineOrKey();

        if (test.IsKey())
            Console.WriteLine(test.KeyName);
        else
            Console.WriteLine($"Output string: {test.OutputString}");
        Console.Read();
    }
}

You can keep updated on this link (is a link to my lib I'm currently working at).

z3nth10n
  • 2,341
  • 2
  • 25
  • 49