Skip to content

Invalid cached console cursor position on Unix #77995

Closed

Description

@qt-kaneko

Description

Console cursor moves incorrectly from the end of the line to its beginning.

Reproduction Steps

Console.CursorLeft = Console.BufferWidth - 1;
Console.Write("2");

Console.CursorLeft = 0;
Console.Write("1");

Expected behavior

1 should be printed at the beginning of the line

Actual behavior

1 is printed after 2 (resize terminal to see this, 1 may be wrapped to the next line)

Regression?

No response

Known Workarounds

Reset cached cursor position, either with moving cursor to [0, 0], or using reflection to invalidate ConsolePal.s_cursorLeft

Configuration

  • .NET 6 macOS 12.6 ARM64
  • .NET 7 macOS 12.6 ARM64
  • .NET 7 Arco Linux x64

Other information

Somewhere in ConsolePal, in things related to the cached cursor position and its calculation.

I think this is due to cached cursor position is computed like on windows (when cursor reaches end of line, and last char is printed, it moves to the next line), while on my tested configuration this is not.

Check ConsolePal.s_cursorLeft and ConsolePal.s_cursorTop with provided example. Cached position is on 0 row instead of Console.WindowWidth row, so it "thinks" that cursor is already at the beginning of the line and there is no need to move it.

Test code:

using System.Diagnostics;
using System.Reflection;

var ConsolePal = Assembly.Load("System.Console").GetType("System.ConsolePal")!;
var s_cursorLeft = ConsolePal.GetField("s_cursorLeft", BindingFlags.NonPublic | BindingFlags.Static)!;
var s_cursorTop = ConsolePal.GetField("s_cursorTop", BindingFlags.NonPublic | BindingFlags.Static)!;

Console.CursorLeft = Console.BufferWidth - 1;
Console.Write("2");

Debug.WriteLine("[X: " + s_cursorLeft.GetValue(null) + "], [Y: " + s_cursorTop.GetValue(null) + "]");

Console.CursorLeft = 0;
Console.Write("1"); // 1 will be printed after 2 instead of at the beginning of the line (resize terminal to see this, 1 may be wrapped to the next line)

Task.Delay(Timeout.Infinite).Wait();

image

image

(On the 2nd image terminal is made wider by 1 symbol)

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Nov 7, 2022
  2. tmds commented on Nov 8, 2022

    @tmds
    Member

    Somewhere in ConsolePal

    It happens here:

    if (left >= width)
    {
    left = 0;
    IncrementY();
    }

    A reproducer without resizing:

    Console.WriteLine("Cached:");
    Console.CursorLeft = Console.BufferWidth - 1;
    Console.Write("2");
    Console.CursorLeft = 0;
    Console.Write("1");
    Console.WriteLine();
    
    Console.WriteLine("Invalidated cache:");
    Console.CursorLeft = Console.BufferWidth - 1;
    Console.Write((char)0xA3);
    Console.CursorLeft = 0;
    Console.Write("1");
    Console.WriteLine();

    You'd expect the same number of lines to be printed in both cases, but that's not the true.
    In the terminal window (on Linux) you see:

    Cached position:
                                             2
    1
    Invalidated position:
    1                                        £
    

    And after resizing:

    Cached position:
                                             21
    Invalidated position:
    1                                        £
    

    I think this is due to cached cursor position is computed like on windows

    Have you tried what happens on Windows? Can you (or someone with a Windows machine) run the above code on Windows and share the output (before and after resizing the window)?

  3. adamsitnik commented on Nov 8, 2022

    @adamsitnik
    Member

    Can you (or someone with a Windows machine) run the above code on Windows and share the output (before and after resizing the window)?

    Before resize:

    image

    After resize:

    image

    Running again after resize:

    image

  4. qt-kaneko commented on Nov 8, 2022

    @qt-kaneko
    Author
  5. qt-kaneko commented on Nov 8, 2022

    @qt-kaneko
    Author

    I think, trying to provide simpler example, I have broke it (due to windows moves cursor to next line, we need to reset it back to the initial line, so Console.CursorLeft is not enough), sorry.

    This code should work:

    Console.Clear();
    
    Console.SetCursorPosition(Console.WindowWidth - 1, Console.WindowHeight - 1);
    Console.Write("2");
    
    Console.SetCursorPosition(0, Console.WindowHeight - 1);
    Console.Write("1");
    

    Without invalidation:

    Windows:

    Before resizing:
    image

    After resizing:
    image

    macOS (the same on Arco Linux):

    Before resizing:
    image

    After resizing:
    image

    With invalidation:

    macOS (the same on Arco Linux):

    Before resizing:
    image

    After resizing:
    image

    Can you (or someone with a Windows machine) run the above code on Windows and share the output (before and after resizing the window)?

    Wrapping enabled:

    Before resize image

    After resize + re-run image

    Wrapping disabled:

    image
  6. tmds commented on Nov 16, 2022

    @tmds
    Member

    On Windows the behavior is the same as for the invalidated character on Linux: even when the characters are printed on the next line due to wrapping, the terminal still considers them to be part of the previous 'logical' line.

    As we have no idea of what a 'logical' line is, we should invalidate the cache when we're at the window boundary.

  7. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Nov 16, 2022
  8. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Dec 13, 2022
  9. ghost removed
    untriagedNew issue has not been triaged by the area owner
    on Dec 13, 2022
  10. added this to the 8.0.0 milestone on Dec 13, 2022
  11. ghost locked as resolved and limited conversation to collaborators on Jan 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions