14

I want to read an users input into a string while still reacting on ESC press at any time, but without defining a system wide hotkey.

So when the user types e. g. "Test Name" but instead of confirming with ENTER presses ESC he should be led back into main menu.

Console.Write("Enter name: ")
if (Console.ReadLine().Contains(ConsoleKey.Escape.ToString()))
{
    goto MainMenu;
}
return Console.ReadLine();

Thats the simplest way I could think of, but since ESC is not seen by Console.ReadLine() it is not working.

Found a rather complex way to react on ESC when pressed before starting to enter text here, but I want it to work at any time.

Community
  • 1
  • 1
AstronAUT
  • 639
  • 2
  • 7
  • 11

6 Answers6

13

You will probably have to forego the use of ReadLine and roll your own using ReadKey:

static void Main(string[] args)
{
    Console.Clear();
    Console.Write("Enter your name and press ENTER.  (ESC to cancel): ");
    string name = readLineWithCancel();

    Console.WriteLine("\r\n{0}", name == null ? "Cancelled" : name);

    Console.ReadLine();
}

//Returns null if ESC key pressed during input.
private static string readLineWithCancel()
{
    string result = null;

    StringBuilder buffer = new StringBuilder();

    //The key is read passing true for the intercept argument to prevent
    //any characters from displaying when the Escape key is pressed.
    ConsoleKeyInfo info = Console.ReadKey(true);
    while (info.Key != ConsoleKey.Enter && info.Key != ConsoleKey.Escape)
    {
        Console.Write(info.KeyChar);
        buffer.Append(info.KeyChar);
        info = Console.ReadKey(true);
    } 

    if (info.Key == ConsoleKey.Enter)
    {
        result = buffer.ToString();
    }

    return result;
}

This code is not complete and may require work to make it robust, but it should give you some ideas.

Chris Dunaway
  • 10,974
  • 4
  • 36
  • 48
  • 1
    Thank you for your response. Looks more efficient than mine, although I had to add support for backspace ;) – AstronAUT Aug 13 '15 at 23:24
  • This doesn't seem to work with the backspace key. It moves the cursor back, but doesn't delete the letter – derekantrican May 17 '17 at 03:35
  • the answer of tinymike below is more sophisticated and works straight out of the box...his answer deserves some upvotes as well :-) – GJEEE Jul 29 '23 at 01:11
5

A bit improved version of Chris Dunaway's one :

    public static bool CancelableReadLine(out string value)
    {
        value = string.Empty;
        var buffer = new StringBuilder();
        var key = Console.ReadKey(true);
        while (key.Key != ConsoleKey.Enter && key.Key != ConsoleKey.Escape)
        {
            if (key.Key == ConsoleKey.Backspace && Console.CursorLeft > 0)
            {
                var cli = --Console.CursorLeft;
                buffer.Remove(cli, 1);
                Console.CursorLeft = 0;
                Console.Write(new String(Enumerable.Range(0, buffer.Length + 1).Select(o => ' ').ToArray()));
                Console.CursorLeft = 0;
                Console.Write(buffer.ToString());
                Console.CursorLeft = cli;
                key = Console.ReadKey(true);
            }
            else if (Char.IsLetterOrDigit(key.KeyChar) || Char.IsWhiteSpace(key.KeyChar))
            {
                var cli = Console.CursorLeft;
                buffer.Insert(cli, key.KeyChar);
                Console.CursorLeft = 0;
                Console.Write(buffer.ToString());
                Console.CursorLeft = cli + 1;
                key = Console.ReadKey(true);
            }
            else if (key.Key == ConsoleKey.LeftArrow && Console.CursorLeft > 0)
            {
                Console.CursorLeft--;
                key = Console.ReadKey(true);
            }
            else if (key.Key == ConsoleKey.RightArrow && Console.CursorLeft < buffer.Length)
            {
                Console.CursorLeft++;
                key = Console.ReadKey(true);
            }
            else
            {
                key = Console.ReadKey(true);
            }
        }

        if (key.Key == ConsoleKey.Enter)
        {
            Console.WriteLine();
            value = buffer.ToString();
            return true;
        }
        return false;
    }
}

I didn't test it much, but at least works for me.

oleg wx
  • 286
  • 5
  • 9
2

There's probably a lot more to improve on but I think I covered most of the basics with this solution:

This code handles:

  • writing beyond the Console window edge
  • calling method after existing text (using Console.Write)
  • resizing Console window
  • Ctrl modifiers

This code does not handle:

  • Tab key (Normal Console.ReadLine has a bug when tabbing further than window size so chose to exclude this as it is seldom needed...)

My code is based off of oleg wx and Chris Dunaway's answers so credit goes to them as well.

I would recommend to try to build your own based off of these answers but sometimes you just need something to work, in that case this code worked for me when I used it in my application:


Just create a new static class, for instance:

public static class XConsole
{

}

and paste the following methods in it:

    public static string CancelableReadLine(out bool isCancelled)
    {
        var cancelKey = ConsoleKey.Escape;
        var builder = new StringBuilder();
        var cki = Console.ReadKey(true);
        int index = 0;
        (int left, int top) startPosition;

        while (cki.Key != ConsoleKey.Enter && cki.Key != cancelKey)
        {
            if (cki.Key == ConsoleKey.LeftArrow)
            {
                if (index < 1)
                {
                    cki = Console.ReadKey(true);
                    continue;
                }

                LeftArrow(ref index, cki);
            }
            else if (cki.Key == ConsoleKey.RightArrow)
            {
                if (index >= builder.Length)
                {
                    cki = Console.ReadKey(true);
                    continue;
                }

                RightArrow(ref index, cki, builder);
            }
            else if (cki.Key == ConsoleKey.Backspace)
            {
                if (index < 1)
                {
                    cki = Console.ReadKey(true);
                    continue;
                }

                BackSpace(ref index, cki, builder);
            }
            else if (cki.Key == ConsoleKey.Delete)
            {
                if (index >= builder.Length)
                {
                    cki = Console.ReadKey(true);
                    continue;
                }

                Delete(ref index, cki, builder);
            }
            else if (cki.Key == ConsoleKey.Tab)
            {
                cki = Console.ReadKey(true);
                continue;
            }
            else
            {
                if (cki.KeyChar == '\0')
                {
                    cki = Console.ReadKey(true);
                    continue;
                }

                Default(ref index, cki, builder);
            }

            cki = Console.ReadKey(true);
        }

        if (cki.Key == cancelKey)
        {
            startPosition = GetStartPosition(index);
            ErasePrint(builder, startPosition);

            isCancelled = true;
            return string.Empty;
        }

        isCancelled = false;

        startPosition = GetStartPosition(index);
        var endPosition = GetEndPosition(startPosition.left, builder.Length);
        var left = 0;
        var top = startPosition.top + endPosition.top + 1;

        Console.SetCursorPosition(left, top);

        var value = builder.ToString();
        return value;
    }

    private static void LeftArrow(ref int index, ConsoleKeyInfo cki)
    {
        var previousIndex = index;
        index--;

        if (cki.Modifiers == ConsoleModifiers.Control)
        {
            index = 0;

            var startPosition = GetStartPosition(previousIndex);
            Console.SetCursorPosition(startPosition.left, startPosition.top);

            return;
        }

        if (Console.CursorLeft > 0)
            Console.CursorLeft--;
        else
        {
            Console.CursorTop--;
            Console.CursorLeft = Console.BufferWidth - 1;
        }
    }

    private static void RightArrow(ref int index, ConsoleKeyInfo cki, StringBuilder builder)
    {
        var previousIndex = index;
        index++;

        if (cki.Modifiers == ConsoleModifiers.Control)
        {
            index = builder.Length;

            var startPosition = GetStartPosition(previousIndex);
            var endPosition = GetEndPosition(startPosition.left, builder.Length);
            var top = startPosition.top + endPosition.top;
            var left = endPosition.left;

            Console.SetCursorPosition(left, top);

            return;
        }

        if (Console.CursorLeft < Console.BufferWidth - 1)
            Console.CursorLeft++;
        else
        {
            Console.CursorTop++;
            Console.CursorLeft = 0;
        }
    }

    private static void BackSpace(ref int index, ConsoleKeyInfo cki, StringBuilder builder)
    {
        var previousIndex = index;
        index--;

        var startPosition = GetStartPosition(previousIndex);
        ErasePrint(builder, startPosition);

        builder.Remove(index, 1);
        Console.Write(builder.ToString());

        GoBackToCurrentPosition(index, startPosition);
    }

    private static void Delete(ref int index, ConsoleKeyInfo cki, StringBuilder builder)
    {
        var startPosition = GetStartPosition(index);
        ErasePrint(builder, startPosition);

        if (cki.Modifiers == ConsoleModifiers.Control)
        {
            builder.Remove(index, builder.Length - index);
            Console.Write(builder.ToString());

            GoBackToCurrentPosition(index, startPosition);
            return;
        }

        builder.Remove(index, 1);
        Console.Write(builder.ToString());

        GoBackToCurrentPosition(index, startPosition);
    }

    private static void Default(ref int index, ConsoleKeyInfo cki, StringBuilder builder)
    {
        var previousIndex = index;
        index++;

        builder.Insert(previousIndex, cki.KeyChar);

        var startPosition = GetStartPosition(previousIndex);
        Console.SetCursorPosition(startPosition.left, startPosition.top);
        Console.Write(builder.ToString());

        GoBackToCurrentPosition(index, startPosition);
    }

    private static (int left, int top) GetStartPosition(int previousIndex)
    {
        int top;
        int left;

        if (previousIndex <= Console.CursorLeft)
        {
            top = Console.CursorTop;
            left = Console.CursorLeft - previousIndex;
        }
        else
        {
            var decrementValue = previousIndex - Console.CursorLeft;
            var rowsFromStart = decrementValue / Console.BufferWidth;
            top = Console.CursorTop - rowsFromStart;
            left = decrementValue - rowsFromStart * Console.BufferWidth;

            if (left != 0)
            {
                top--;
                left = Console.BufferWidth - left;
            }
        }

        return (left, top);
    }

    private static void GoBackToCurrentPosition(int index, (int left, int top) startPosition)
    {
        var rowsToGo = (index + startPosition.left) / Console.BufferWidth;
        var rowIndex = index - rowsToGo * Console.BufferWidth;

        var left = startPosition.left + rowIndex;
        var top = startPosition.top + rowsToGo;

        Console.SetCursorPosition(left, top);
    }

    private static (int left, int top) GetEndPosition(int startColumn, int builderLength)
    {
        var cursorTop = (builderLength + startColumn) / Console.BufferWidth;
        var cursorLeft = startColumn + (builderLength - cursorTop * Console.BufferWidth);

        return (cursorLeft, cursorTop);
    }

    private static void ErasePrint(StringBuilder builder, (int left, int top) startPosition)
    {
        Console.SetCursorPosition(startPosition.left, startPosition.top);
        Console.Write(new string(Enumerable.Range(0, builder.Length).Select(o => ' ').ToArray()));

        Console.SetCursorPosition(startPosition.left, startPosition.top);
    }

You can now call it like this:

        Console.WriteLine("Calling at start of screen");
        string text = XConsole.CancelableReadLine(out bool isCancelled);

        if (isCancelled)
        {
            //Do what you want in here, for instance: 
            return;
        }

You can also call it after a Console.Write:

        Console.WriteLine("Calling after Console.Write");
        Console.Write("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
        string text = XConsole.CancelableReadLine(out bool isCancelled);

        if (isCancelled) 
            return;

Hope this helps!

tiny_mike
  • 225
  • 3
  • 8
1

A bit improved version of oleg wx

Now it can be used in such a case

static void Main(string[] args)
{
    Console.Write("Text: ");
    CancelableReadConsoleLine(out string value);
}

public static bool CancelableReadConsoleLine(out string line)
{
    var clOffset = Console.CursorLeft;
    line = string.Empty;
    var buffer = new StringBuilder();
    var key = Console.ReadKey(true);
    while (key.Key != ConsoleKey.Enter && key.Key != ConsoleKey.Escape)
    {
        if (key.Key == ConsoleKey.Backspace && Console.CursorLeft - clOffset > 0)
        {
            var cli = Console.CursorLeft - clOffset - 1;
            buffer.Remove(cli, 1);
            Console.CursorLeft = clOffset;
            Console.Write(new string(' ', buffer.Length + 1));
            Console.CursorLeft = clOffset;
            Console.Write(buffer.ToString());
            Console.CursorLeft = cli + clOffset;
            key = Console.ReadKey(true);
        }
        else if (key.Key == ConsoleKey.Delete && Console.CursorLeft - clOffset < buffer.Length)
        {
            var cli = Console.CursorLeft - clOffset;
            buffer.Remove(cli, 1);
            Console.CursorLeft = clOffset;
            Console.Write(new string(' ', buffer.Length + 1));
            Console.CursorLeft = clOffset;
            Console.Write(buffer.ToString());
            Console.CursorLeft = cli + clOffset;
            key = Console.ReadKey(true);
        }
        else if (char.IsLetterOrDigit(key.KeyChar) || char.IsWhiteSpace(key.KeyChar))
        {
            var cli = Console.CursorLeft - clOffset;
            buffer.Insert(cli, key.KeyChar);
            Console.CursorLeft = clOffset;
            Console.Write(buffer.ToString());
            Console.CursorLeft = cli + clOffset + 1;
            key = Console.ReadKey(true);
        }
        else if (key.Key == ConsoleKey.LeftArrow && Console.CursorLeft - clOffset > 0)
        {
            Console.CursorLeft--;
            key = Console.ReadKey(true);
        }
        else if (key.Key == ConsoleKey.RightArrow && Console.CursorLeft - clOffset < buffer.Length)
        {
            Console.CursorLeft++;
            key = Console.ReadKey(true);
        }
        else
        {
            key = Console.ReadKey(true);
        }
    }

    if (key.Key == ConsoleKey.Enter)
    {
        Console.WriteLine();
        line = buffer.ToString();
        return true;
    }
    return false;
}
Oleh
  • 11
  • 2
0

In case someone has the same problem. Found an solution by my own. Could be a little shorter, but at least it works:

                string name;
                char CurrentChar = ' ';                    
                do
                {
                    CurrentChar = Console.ReadKey().KeyChar;
                    if (CurrentChar != '\b')
                    {
                        name = name + CurrentChar;
                        if (CurrentChar == (char)13)
                        {
                            return name;
                        }
                    }
                    else if (Console.CursorLeft >= 14)
                    {
                        name = name.Remove(name.Length - 1);
                        Console.Write(" \b");
                    }
                    else
                    {
                        Console.CursorLeft = 14;
                    }

                } while (CurrentChar != (char)27);
AstronAUT
  • 639
  • 2
  • 7
  • 11
-2

This is the simplest way:

Console.WriteLine("Press ESC to go to Main Menu");

while (!(Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Escape))
{
    // your code here
}
goto MainMenu;
Jonathan Carroll
  • 880
  • 1
  • 5
  • 20
  • 2
    That is not working for me: when the while loop is entered `Console.ReadLine()` is stoping the thread until ENTER is pressed. – AstronAUT Aug 13 '15 at 19:55