Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Console Application - WriteLine above current working line

I have seen a few other posts very similar to this one, but the answers they give are not correctly answering the question. Sorry if there is something hidden away that I couldnt find...

I want to use Console.WriteLine() to print something above my current Console.ReadLine(), for example, my app prints "Hello world" and starts a thread that (in 5 seconds) will print "I just waited 5 seconds" above the line where I need to input something, like this:

Hello world
Please input something: _

Then 5 seconds will pass and it will look like this:

Hello world
I just waited 5 seconds
Please input something: _

So far I've tried using Console.SetCursorPosition(0,Console.CursorTop - 1) but this just prints over the line "Please input something: _" and if I use Console.CursorTop - 2 instead it crashes saying "[2] Out of range" (no idea why this is) and if I use Console.CursorTop - 2 it prints under "Please input something: _"... so my question is how do I print something ABOVE the line "Please input something: _"

like image 943
Lachlan Mather Avatar asked Feb 08 '17 10:02

Lachlan Mather


1 Answers

Just moving the cursor is not good enough, the problem is that you are inserting text. That is possible, the Console.MoveBufferArea() method gives you access to the underlying screen buffer of the console and lets you move text and attributes to another line.

There are a couple of tricky corner-cases. One you already found, you have to force the console to scroll if the cursor is located at the end of the buffer. And the timer is a very difficult problem to solve, you can only really do this correctly if you can prevent Console.ReadLine() from moving the cursor at the exact same time that the timer's Elapsed event inserts the text. That requires a lock, you cannot insert a lock in Console.ReadLine().

Some sample code you can play with to get you there:

static string TimedReadline(string prompt, int seconds) {
    int y = Console.CursorTop;
    // Force a scroll if we're at the end of the buffer
    if (y == Console.BufferHeight - 1) {
        Console.WriteLine();
        Console.SetCursorPosition(0, --y);
    }
    // Setup the timer
    using (var tmr = new System.Timers.Timer(1000 * seconds)) {
        tmr.AutoReset = false;
        tmr.Elapsed += (s, e) => {
            if (Console.CursorTop != y) return;
            int x = Cursor.Left;
            Console.MoveBufferArea(0, y, Console.WindowWidth, 1, 0, y + 1);
            Console.SetCursorPosition(0, y);
            Console.Write("I just waited {0} seconds", seconds);
            Console.SetCursorPosition(x, y + 1);
        };
        tmr.Enabled = true;
        // Write the prompt and obtain the user's input
        Console.Write(prompt);
        return Console.ReadLine();
    }
}

Sample usage:

static void Main(string[] args) {
    for (int ix = 0; ix < Console.BufferHeight; ++ix) Console.WriteLine("Hello world");
    var input = TimedReadline("Please input something: ", 2);
}

Note the test on the Console.Top property, it ensures that nothing goes drastically wrong when the user typed too much text and forced a scroll or if Console.ReadLine() completed at the exact same time that the timer ticked. Proving that it is thread-safe in all possible cases is hard to do, there will surely be trouble when Console.ReadLine() moves the cursor horizontally at the exact same time that the Elapsed event handler runs. I recommend you write your own Console.ReadLine() method so you can insert the lock and feel confident it is always safe.

like image 162
Hans Passant Avatar answered Sep 20 '22 13:09

Hans Passant