Cursor jumps near end of line when using alternate screen buffer and raw mode #12012

Closed
opened 2026-01-31 03:03:55 +00:00 by claunia · 5 comments
Owner

Originally created by @clinton-r on GitHub (Jan 11, 2021).

Environment

Microsoft Windows [Version 10.0.19041.685]
Windows Terminal Version: 1.4.3243.0
Microsoft Visual Studio Community 2019 Version 16.8.3

Steps to reproduce

Compile the following program. Run it in Windows Terminal, either in a cmd or Powershell tab.

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

static HANDLE hStdin = NULL;
static HANDLE hStdout = NULL;
static int savedConsoleOutputModeIsValid = 0;
static DWORD savedConsoleOutputMode = 0;
static int savedConsoleInputModeIsValid = 0;
static DWORD savedConsoleInputMode = 0;

static void restoreConsoleState(void)
{
    if (savedConsoleOutputModeIsValid)
    {
        SetConsoleMode(hStdout, savedConsoleOutputMode);
    }
    if (savedConsoleInputModeIsValid)
    {
        SetConsoleMode(hStdin, savedConsoleInputMode);
    }
}

int main(void)
{
    // Make sure the console state will be returned to its original state
    // when this program ends
    atexit(restoreConsoleState);

    // Get handles for stdin and stdout
    hStdin = GetStdHandle(STD_INPUT_HANDLE);
    if (hStdin == INVALID_HANDLE_VALUE || hStdin == NULL)
    {
        puts("Failed to get handle for stdin.  Exiting.");
        return 1;
    }
    hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
    if (hStdout == INVALID_HANDLE_VALUE || hStdout == NULL)
    {
        puts("Failed to get handle for stdout.  Exiting.");
        return 1;
    }

    // Set console to "raw" mode

    if (!GetConsoleMode(hStdout, &savedConsoleOutputMode))
    {
        puts("Failed to get console mode for stdout.  Exiting.");
        return 1;
    }
    savedConsoleOutputModeIsValid = 1;
    DWORD newOutputMode = savedConsoleOutputMode;
    newOutputMode &= ~ENABLE_PROCESSED_OUTPUT;
    newOutputMode &= ~ENABLE_WRAP_AT_EOL_OUTPUT;
    newOutputMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
    if (!SetConsoleMode(hStdout, newOutputMode))
    {
        puts("Failed to set console mode for stdout.  Exiting.");
        return 1;
    }

    if (!GetConsoleMode(hStdin, &savedConsoleInputMode))
    {
        puts("Failed to get console mode for stdin.  Exiting.");
        return 1;
    }
    savedConsoleInputModeIsValid = 1;
    DWORD newInputMode = savedConsoleInputMode;
    newInputMode &= ~ENABLE_ECHO_INPUT;
    newInputMode &= ~ENABLE_LINE_INPUT;
    newInputMode &= ~ENABLE_PROCESSED_INPUT;
    newInputMode |= ENABLE_VIRTUAL_TERMINAL_INPUT;
    if (!SetConsoleMode(hStdin, newInputMode))
    {
        puts("Failed to set console mode for stdin.  Exiting.");
        return 1;
    }

    // Set alternate screen buffer
    printf("\x1b[?1049h");
    
    // Clear the screen and go to home position
    printf("\x1b[2J");
    printf("\x1b[H");
    
    // Show the modes
    printf("out: %08x->%08x in: %08x->%08x ",
        (int)savedConsoleOutputMode, (int)newOutputMode,
        (int)savedConsoleInputMode, (int)newInputMode);
    
    // Print a string to show what happens
    char msg[] = "The early bird catches the worm.  "
                "The quick brown fox jumps over the lazy dog.";
                
    for (char* p = &msg[0]; *p; p++)
    {
        Sleep(250);
        printf("%c", *p);
    }
    
    // Set main screen buffer
    printf("\x1b[?1049l");

    return 0;
}

Expected behavior

The text should be written all the way to the end of the first line, then the remaining characters should be written at the last position on the first line.

Actual behavior

When the text being written gets near the end of the line, the cursor jumps to the last position of another line and the rest of the characters are written at that position.

Extra Notes

Seems to only happen in raw mode.
Seems to only happen in alternate screen buffer.
I think the line that the cursor jumps to is the line it was on when the program was run from the prompt.

Originally created by @clinton-r on GitHub (Jan 11, 2021). <!-- 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 I ACKNOWLEDGE THE FOLLOWING BEFORE PROCEEDING: 1. If I delete this entire template and go my own path, the core team may close my issue without further explanation or engagement. 2. If I list multiple bugs/concerns in this one issue, the core team may close my issue without further explanation or engagement. 3. If I write an issue that has many duplicates, the core team may close my issue without further explanation or engagement (and without necessarily spending time to find the exact duplicate ID number). 4. If I leave the title incomplete when filing the issue, the core team may close my issue without further explanation or engagement. 5. If I file something completely blank in the body, the core team may close my issue without further explanation or engagement. All good? Then proceed! --> <!-- This bug tracker is monitored by Windows Terminal development team and other technical folks. **Important: When reporting BSODs or security issues, DO NOT attach memory dumps, logs, or traces to Github issues**. Instead, send dumps/traces to secure@microsoft.com, referencing this GitHub issue. If this is an application crash, please also provide a Feedback Hub submission link so we can find your diagnostic data on the backend. Use the category "Apps > Windows Terminal (Preview)" and choose "Share My Feedback" after submission to get the link. Please use this form and describe your issue, concisely but precisely, with as much detail as possible. --> # Environment Microsoft Windows [Version 10.0.19041.685] Windows Terminal Version: 1.4.3243.0 Microsoft Visual Studio Community 2019 Version 16.8.3 # Steps to reproduce <!-- A description of how to trigger this bug. --> Compile the following program. Run it in Windows Terminal, either in a cmd or Powershell tab. ```C #include <stdio.h> #include <stdlib.h> #include <windows.h> static HANDLE hStdin = NULL; static HANDLE hStdout = NULL; static int savedConsoleOutputModeIsValid = 0; static DWORD savedConsoleOutputMode = 0; static int savedConsoleInputModeIsValid = 0; static DWORD savedConsoleInputMode = 0; static void restoreConsoleState(void) { if (savedConsoleOutputModeIsValid) { SetConsoleMode(hStdout, savedConsoleOutputMode); } if (savedConsoleInputModeIsValid) { SetConsoleMode(hStdin, savedConsoleInputMode); } } int main(void) { // Make sure the console state will be returned to its original state // when this program ends atexit(restoreConsoleState); // Get handles for stdin and stdout hStdin = GetStdHandle(STD_INPUT_HANDLE); if (hStdin == INVALID_HANDLE_VALUE || hStdin == NULL) { puts("Failed to get handle for stdin. Exiting."); return 1; } hStdout = GetStdHandle(STD_OUTPUT_HANDLE); if (hStdout == INVALID_HANDLE_VALUE || hStdout == NULL) { puts("Failed to get handle for stdout. Exiting."); return 1; } // Set console to "raw" mode if (!GetConsoleMode(hStdout, &savedConsoleOutputMode)) { puts("Failed to get console mode for stdout. Exiting."); return 1; } savedConsoleOutputModeIsValid = 1; DWORD newOutputMode = savedConsoleOutputMode; newOutputMode &= ~ENABLE_PROCESSED_OUTPUT; newOutputMode &= ~ENABLE_WRAP_AT_EOL_OUTPUT; newOutputMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; if (!SetConsoleMode(hStdout, newOutputMode)) { puts("Failed to set console mode for stdout. Exiting."); return 1; } if (!GetConsoleMode(hStdin, &savedConsoleInputMode)) { puts("Failed to get console mode for stdin. Exiting."); return 1; } savedConsoleInputModeIsValid = 1; DWORD newInputMode = savedConsoleInputMode; newInputMode &= ~ENABLE_ECHO_INPUT; newInputMode &= ~ENABLE_LINE_INPUT; newInputMode &= ~ENABLE_PROCESSED_INPUT; newInputMode |= ENABLE_VIRTUAL_TERMINAL_INPUT; if (!SetConsoleMode(hStdin, newInputMode)) { puts("Failed to set console mode for stdin. Exiting."); return 1; } // Set alternate screen buffer printf("\x1b[?1049h"); // Clear the screen and go to home position printf("\x1b[2J"); printf("\x1b[H"); // Show the modes printf("out: %08x->%08x in: %08x->%08x ", (int)savedConsoleOutputMode, (int)newOutputMode, (int)savedConsoleInputMode, (int)newInputMode); // Print a string to show what happens char msg[] = "The early bird catches the worm. " "The quick brown fox jumps over the lazy dog."; for (char* p = &msg[0]; *p; p++) { Sleep(250); printf("%c", *p); } // Set main screen buffer printf("\x1b[?1049l"); return 0; } ``` # Expected behavior <!-- A description of what you're expecting, possibly containing screenshots or reference material. --> The text should be written all the way to the end of the first line, then the remaining characters should be written at the last position on the first line. # Actual behavior <!-- What's actually happening? --> When the text being written gets near the end of the line, the cursor jumps to the last position of another line and the rest of the characters are written at that position. # Extra Notes Seems to only happen in raw mode. Seems to only happen in alternate screen buffer. I think the line that the cursor jumps to is the line it was on when the program was run from the prompt.
Author
Owner

@zadjii-msft commented on GitHub (Jan 11, 2021):

Weird. Great repro though, something we can actually use to write a test for. Might have something to do with #381 perhaps.

@zadjii-msft commented on GitHub (Jan 11, 2021): Weird. Great repro though, something we can actually use to write a test for. Might have something to do with #381 perhaps.
Author
Owner

@clinton-r commented on GitHub (Jan 13, 2021):

In the response to #8750 I learned (thanks @DHowett!) that I should keep ENABLE_PROCESSED_OUTPUT set on stdout when using ENABLE_VIRTUAL_TERMINAL_PROCESSING. If I change the example above to do that, the cursor jumping problem goes away. However, the output now wraps at the end of the line, even though ENABLE_WRAP_AT_EOL_OUTPUT is off. I tried also replacing all the printf()s with calls to WriteConsoleA() (and sprintf_s), but still it wraps. Is there anything obvious I'm still missing? Thanks!

@clinton-r commented on GitHub (Jan 13, 2021): In the response to #8750 I learned (thanks @DHowett!) that I should keep ENABLE_PROCESSED_OUTPUT set on stdout when using ENABLE_VIRTUAL_TERMINAL_PROCESSING. If I change the example above to do that, the cursor jumping problem goes away. However, the output now wraps at the end of the line, even though ENABLE_WRAP_AT_EOL_OUTPUT is off. I tried also replacing all the printf()s with calls to WriteConsoleA() (and sprintf_s), but still it wraps. Is there anything obvious I'm still missing? Thanks!
Author
Owner

@j4james commented on GitHub (Mar 25, 2023):

FYI, this is a duplicate of #4363. You can see what's happening if you run the test app in conhost - you'll get something like this:

[?1049hout: 00000007->00000004 in: 000001f7->000003f0 The early bird cal

But when run from Windows Terminal, those buffer contents need to be forwarded over conpty, and you've got raw ESC characters in the buffer that get interpreted as actual escape sequences. So now the switch to the alt buffer, the erase display, and the home sequence are all executed, and the text starts writing in the top left of the viewport.

But conhost doesn't know that that has happened, so it reaches the end of the line earlier than you would expect (because you aren't seeing the initial characters). And when it needs to reposition the cursor at the end of the line to stop it wrapping, it now moves to the "correct" position, which is further down the viewport.

I wrote a hacky fix for #4363 a while back and I've tested this app in Windows Terminal with that patch applied - it then works exactly the same as in conhost.

@j4james commented on GitHub (Mar 25, 2023): FYI, this is a duplicate of #4363. You can see what's happening if you run the test app in conhost - you'll get something like this: [?1049hout: 00000007->00000004 in: 000001f7->000003f0 The early bird cal But when run from Windows Terminal, those buffer contents need to be forwarded over conpty, and you've got raw `ESC` characters in the buffer that get interpreted as actual escape sequences. So now the switch to the alt buffer, the erase display, and the home sequence are all executed, and the text starts writing in the top left of the viewport. But conhost doesn't know that that has happened, so it reaches the end of the line earlier than you would expect (because you aren't seeing the initial characters). And when it needs to reposition the cursor at the end of the line to stop it wrapping, it now moves to the "correct" position, which is further down the viewport. I wrote a hacky fix for #4363 a while back and I've tested this app in Windows Terminal with that patch applied - it then works exactly the same as in conhost.
Author
Owner

@zadjii-msft commented on GitHub (Mar 27, 2023):

Oh duh good point. Thanks for the investigation!

/dup #4363

@zadjii-msft commented on GitHub (Mar 27, 2023): Oh duh good point. Thanks for the investigation! /dup #4363
Author
Owner

@microsoft-github-policy-service[bot] commented on GitHub (Mar 27, 2023):

Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report!

@microsoft-github-policy-service[bot] commented on GitHub (Mar 27, 2023): Hi! We've identified this issue as a duplicate of another one that already exists on this Issue Tracker. This specific instance is being closed in favor of tracking the concern over on the referenced thread. Thanks for your report!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#12012