Saved cursor position (CSI s) not restored (CSI u) #22632

Closed
opened 2026-01-31 08:19:02 +00:00 by claunia · 16 comments
Owner

Originally created by @vefatica on GitHub (Dec 8, 2024).

Windows Terminal version

1.22.2411.18002 (preview)

Windows build number

10.0.19045.5131 (2009, 22H2)

Other Software

Source for stand-alone test attached. No other software needed.

Steps to reproduce

I have a test app (zipped source attached) which temporarily provides line numbers (in WT or a console). It works like this:

Save the rightmost 3 columns of the terminal
Turn off the cursor and save its position (L"\x1b[?25l" L"\x1b[s")
In each row (i), add 3 spaces at the front and write line numbers (wsprintf(szSequence, L"\x1b[%d;1H" L"\x1b[3@" L"\x1b[1G" L"%02d ", i, i);)

Sleep(3000); later ...

In each row (i) , get rid of the line numbers (wsprintf(szSequence, L"\x1b[%d;1H" L"\x1b[3P", i);)
Restore the rightmost 3 columns.
Restore the cursor position and turn it back on

Sleep(3000);

When run in a console, the cursor position has been restored during the second sleep. When run in WT, the cursor has not been restored during the second sleep; it's at the beginning of the last row..

I believe that, a couple years ago, this worked correctly in WT.

linenos.zip

Expected Behavior

Same in WT and console

Actual Behavior

When run in a console, the cursor position has been restored during the second sleep. When run in WT, the cursor has not been restored during the second sleep; it's at the beginning of the last row..

Originally created by @vefatica on GitHub (Dec 8, 2024). ### Windows Terminal version 1.22.2411.18002 (preview) ### Windows build number 10.0.19045.5131 (2009, 22H2) ### Other Software Source for stand-alone test attached. No other software needed. ### Steps to reproduce I have a test app (zipped source attached) which temporarily provides line numbers (in WT or a console). It works like this: Save the rightmost 3 columns of the terminal Turn off the cursor and save its position (L"\x1b[?25l" L"\x1b[s") In each row (i), add 3 spaces at the front and write line numbers (wsprintf(szSequence, L"\x1b[%d;1H" L"\x1b[3@" L"\x1b[1G" L"%02d ", i, i);) Sleep(3000); later ... In each row (i) , get rid of the line numbers (wsprintf(szSequence, L"\x1b[%d;1H" L"\x1b[3P", i);) Restore the rightmost 3 columns. Restore the cursor position and turn it back on Sleep(3000); When run in a console, the cursor position has been restored during the second sleep. When run in WT, the cursor has not been restored during the second sleep; it's at the beginning of the last row.. I believe that, a couple years ago, this worked correctly in WT. [linenos.zip](https://github.com/user-attachments/files/18053165/linenos.zip) ### Expected Behavior Same in WT and console ### Actual Behavior When run in a console, the cursor position has been restored during the second sleep. When run in WT, the cursor has not been restored during the second sleep; it's at the beginning of the last row..
claunia added the Needs-TriageIssue-Bug labels 2026-01-31 08:19:02 +00:00
Author
Owner

@vefatica commented on GitHub (Dec 8, 2024):

Apparently, WriteConsoleOutput() is somehow messing with CSI s / CSI u (in WT).

The test app works correctly if I do these in the opposite order.

	// restore the rightmost three columns
	WriteConsoleOutput(con.hOut, pStripe, cdBufSize, cdZero, &srStripe);

	// restore cursor position; cursor on
	con.Emit(L"\x1b[u" L"\x1b[?25h");
@vefatica commented on GitHub (Dec 8, 2024): Apparently, WriteConsoleOutput() is somehow messing with `CSI s` / `CSI u` (in WT). The test app works correctly if I do these in the opposite order. ``` // restore the rightmost three columns WriteConsoleOutput(con.hOut, pStripe, cdBufSize, cdZero, &srStripe); // restore cursor position; cursor on con.Emit(L"\x1b[u" L"\x1b[?25h"); ```
Author
Owner

@j4james commented on GitHub (Dec 8, 2024):

Apparently, WriteConsoleOutput() is somehow messing with CSI s / CSI u (in WT).

Yeah, I think that's expected. The conpty implementation of WriteConsoleOutput will use a DECSC sequence when translating that call into a VT sequence. If you're mixing VT sequences with legacy console API calls, there's no guarantee that the VT state will be preserved across the API calls.

@j4james commented on GitHub (Dec 8, 2024): > Apparently, WriteConsoleOutput() is somehow messing with `CSI s` / `CSI u` (in WT). Yeah, I think that's expected. The conpty implementation of `WriteConsoleOutput` will use a `DECSC` sequence when translating that call into a VT sequence. If you're mixing VT sequences with legacy console API calls, there's no guarantee that the VT state will be preserved across the API calls.
Author
Owner

@vefatica commented on GitHub (Dec 8, 2024):

Ugh! Can I save/restore a rectangle in the viewport without Read/WriteConsoleOutput.

@vefatica commented on GitHub (Dec 8, 2024): Ugh! Can I save/restore a rectangle in the viewport without Read/WriteConsoleOutput.
Author
Owner

@j4james commented on GitHub (Dec 8, 2024):

Ugh! Can I save/restore a rectangle in the viewport without Read/WriteConsoleOutput.

Yes, you can copy the area you want to save to a background page with a DECCRA sequence, and then copy it back when you want to restore it. By default you should be on page 1, in which case page 2 can be the page where you preserve the saved content.

@j4james commented on GitHub (Dec 8, 2024): > Ugh! Can I save/restore a rectangle in the viewport without Read/WriteConsoleOutput. Yes, you can copy the area you want to save to a background page with a [`DECCRA`](https://vt100.net/docs/vt510-rm/DECCRA.html) sequence, and then copy it back when you want to restore it. By default you should be on page 1, in which case page 2 can be the page where you preserve the saved content.
Author
Owner

@vefatica commented on GitHub (Dec 8, 2024):

Thanks! I'll give that a try.

@vefatica commented on GitHub (Dec 8, 2024): Thanks! I'll give that a try.
Author
Owner

@vefatica commented on GitHub (Dec 8, 2024):

That was easy enough (actually easier than Read/WriteConsoleOutput). How do I get rows/columns in the viewport without the console API? Thanks again.

@vefatica commented on GitHub (Dec 8, 2024): That was easy enough (actually easier than Read/WriteConsoleOutput). How do I get rows/columns in the viewport without the console API? Thanks again.
Author
Owner

@j4james commented on GitHub (Dec 8, 2024):

If you want the full addressable area (which may be wider than the visible viewport if you've got horizontal scrolling enabled in conhost), then the standard way to retrieve the size would be to move the cursor to the bottom right of the windows with a CUP sequence using coordinates like 9999;9999, and then get back the resulting position with a CPR request.

An easier way (but less standard) would be to send a CSI 18 t request, which will return the size of the text areas in characters.

If you want just the visible viewport, then you can use a DECRQDE query, which will return a DECRPDE report containing both the size of the visible window as well as the scroll offset within the wider page area.

@j4james commented on GitHub (Dec 8, 2024): If you want the full addressable area (which may be wider than the visible viewport if you've got horizontal scrolling enabled in conhost), then the standard way to retrieve the size would be to move the cursor to the bottom right of the windows with a [`CUP`](https://vt100.net/docs/vt510-rm/CUP.html) sequence using coordinates like `9999;9999`, and then get back the resulting position with a [`CPR`](https://vt100.net/docs/vt510-rm/CPR.html) request. An easier way (but less standard) would be to send a [`CSI 18 t`](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps;Ps;Ps-t:Ps-=-1-8.2068) request, which will return the size of the text areas in characters. If you want just the visible viewport, then you can use a [`DECRQDE`](https://vt100.net/docs/vt510-rm/DECRQDE.html) query, which will return a [`DECRPDE`](https://vt100.net/docs/vt510-rm/DECRPDE.html) report containing both the size of the visible window as well as the scroll offset within the wider page area.
Author
Owner

@j4james commented on GitHub (Dec 8, 2024):

I should mention that if you're looking up the screen size for use with DECCRA, you probably don't actually need that information. If you just use the default values for the bottom right coordinates (i.e. leave those parameters blank), it should automatically copy the whole page.

@j4james commented on GitHub (Dec 8, 2024): I should mention that if you're looking up the screen size for use with `DECCRA`, you probably don't actually need that information. If you just use the default values for the bottom right coordinates (i.e. leave those parameters blank), it should automatically copy the whole page.
Author
Owner

@vefatica commented on GitHub (Dec 8, 2024):

I suppose the whole viewport would work but I only need the last three columns (and I have that working). But I'm trying to use less (none if possible) of the console API). And I know much more about the Win32 API than about C itself. I may have never dealt with stdin! I'm struggling with reading the response of CSI 18 t using only C. The response is not newline-terminated and I don't know its length in advance. How do I read it with just C (embarrassed to have to ask)? ReadConsole on CONIN$ and a big enough buffer just gets it, no questions asked.

@vefatica commented on GitHub (Dec 8, 2024): I suppose the whole viewport would work but I only need the last three columns (and I have that working). But I'm trying to use less (none if possible) of the console API). And I know much more about the Win32 API than about C itself. I may have never dealt with stdin! I'm struggling with reading the response of `CSI 18 t` using only C. The response is not newline-terminated and I don't know its length in advance. How do I read it with just C (embarrassed to have to ask)? ReadConsole on CONIN$ and a big enough buffer just gets it, no questions asked.
Author
Owner

@j4james commented on GitHub (Dec 8, 2024):

I'm probably not the best person to advise you on this, because I have no idea what I'm doing half the time, but in my own simple terminal projects the only platform specific code I usually require is a getch function that can read a single character of input. For win32, this works as follows:

  1. On startup I setup the input handle with the following flags:

    HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE);
    SetConsoleMode(input_handle, input_mode & ~ENABLE_LINE_INPUT & ~ENABLE_ECHO_INPUT & ~ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT);
    
  2. My getch() function is then just a ReadConsole call like this:

    char ch;
    DWORD chars_read = 0;
    HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE);
    ReadConsoleA(input_handle, &ch, 1, &chars_read, NULL);
    

You'll also want to save and restore the original modes on exit, but that's the main gist of it. And with that functionality in place, if I need to handle a response from a VT query, I just sit in a loop reading characters and adding them to a buffer until I've received the final character.

auto response = std::string{};
for (;;) {
    const auto ch = getch();
    response += ch;
    if (ch == final_char) break;
}

In the case of a CSI 18 t query, the final char of the response will be t. For a CPR request it would be R. For a DECRPDE request it would be w, etc. Once I have the response, I typically use a regex to extract the various parameters.

There are quite likely better ways of doing this, but that's my approach FWIW.

@j4james commented on GitHub (Dec 8, 2024): I'm probably not the best person to advise you on this, because I have no idea what I'm doing half the time, but in my own simple terminal projects the only platform specific code I usually require is a `getch` function that can read a single character of input. For win32, this works as follows: 1. On startup I setup the input handle with the following flags: ```cpp HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE); SetConsoleMode(input_handle, input_mode & ~ENABLE_LINE_INPUT & ~ENABLE_ECHO_INPUT & ~ENABLE_PROCESSED_INPUT | ENABLE_VIRTUAL_TERMINAL_INPUT); ``` 2. My `getch()` function is then just a `ReadConsole` call like this: ```cpp char ch; DWORD chars_read = 0; HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE); ReadConsoleA(input_handle, &ch, 1, &chars_read, NULL); ``` You'll also want to save and restore the original modes on exit, but that's the main gist of it. And with that functionality in place, if I need to handle a response from a VT query, I just sit in a loop reading characters and adding them to a buffer until I've received the _final_ character. ```cpp auto response = std::string{}; for (;;) { const auto ch = getch(); response += ch; if (ch == final_char) break; } ``` In the case of a `CSI 18 t` query, the final char of the response will be `t`. For a `CPR` request it would be `R`. For a `DECRPDE` request it would be `w`, etc. Once I have the response, I typically use a regex to extract the various parameters. There are quite likely better ways of doing this, but that's my approach FWIW.
Author
Owner

@vefatica commented on GitHub (Dec 8, 2024):

Getch() in a loop! Of course; I should have thought of that. Thanks.

I'm sticking with the console API for now (one of those old_dog/new_trick things).

I've removed all the VT stuff except for inserting spaces at BOL and removing them later. I could do that with the API but there seems to be little point in doing so (and CSI 3 @ / CSI 3 P makes it so easy).

One last question: Is there any chance a saved cursor position could persist beyond WriteConsoleOutput (maybe others)? IIRC, something similar was done to make tabstop settings more persistent.

I'll close this issue. You have been a big help @j4james. Thank you.

@vefatica commented on GitHub (Dec 8, 2024): Getch() in a loop! Of course; I should have thought of that. Thanks. I'm sticking with the console API for now (one of those old_dog/new_trick things). I've removed all the VT stuff except for inserting spaces at BOL and removing them later. I could do that with the API but there seems to be little point in doing so (and `CSI 3 @` / `CSI 3 P` makes it so easy). One last question: Is there any chance a saved cursor position could persist beyond WriteConsoleOutput (maybe others)? IIRC, something similar was done to make tabstop settings more persistent. I'll close this issue. You have been a big help @j4james. Thank you.
Author
Owner

@j4james commented on GitHub (Dec 9, 2024):

Is there any chance a saved cursor position could persist beyond WriteConsoleOutput (maybe others)?

I'm not sure. This is more @lhecker's domain. But I believe he has ongoing plans for the conpty architecture that could one day handle console API calls without the need for VT translation (at least on locally hosted applications), and in that case you probably wouldn't lose your saved cursor positions.

@j4james commented on GitHub (Dec 9, 2024): > Is there any chance a saved cursor position could persist beyond WriteConsoleOutput (maybe others)? I'm not sure. This is more @lhecker's domain. But I believe he has ongoing plans for the conpty architecture that could one day handle console API calls without the need for VT translation (at least on locally hosted applications), and in that case you probably wouldn't lose your saved cursor positions.
Author
Owner

@lhecker commented on GitHub (Dec 9, 2024):

FWIW the ideal way to read VT input from the terminal is read chunks of 256 chars, but ideally >=4Ki at a time. I'd then split off any unfinished codepoints & concatenate it with previously unfinished ones. Then I'd run a VT parser on the chunk and store the state of the last finished codepoint. Round-trip-time for stdin reads on UNIX is comparatively high, but for console API calls it's excessively high (all the way up to multiple milliseconds on a loaded system).
But for simple applications this isn't really much of a concern of course.

Is there any chance a saved cursor position could persist beyond WriteConsoleOutput (maybe others)?

What @j4james is right: I do plan to make Windows Terminal behave almost like conhost at least for locally running applications. But I think that's not an assumption you should rely on, because I don't plan to fix it for SSH and other ConPTY scenarios. In my opinion the console APIs are one thing and VT processing is another. Ideally, they should work together in the same application without conflicting, because that allows for seamless migrations to VT. But if there's a good reason for them to not work with each other, I think that's a necessary sacrifice to make. That's also the case here, because DECSC is the optimal way to reliably backup and restore attributes without assuming what the hosting terminal is capable of.

@lhecker commented on GitHub (Dec 9, 2024): FWIW the ideal way to read VT input from the terminal is read chunks of 256 chars, but ideally >=4Ki at a time. I'd then split off any unfinished codepoints & concatenate it with previously unfinished ones. Then I'd run a VT parser on the chunk and store the state of the last finished codepoint. Round-trip-time for stdin reads on UNIX is comparatively high, but for console API calls it's excessively high (all the way up to multiple milliseconds on a loaded system). But for simple applications this isn't really much of a concern of course. > Is there any chance a saved cursor position could persist beyond WriteConsoleOutput (maybe others)? What @j4james is right: I do plan to make Windows Terminal behave almost like conhost at least for locally running applications. But I think that's not an assumption you should rely on, because I don't plan to fix it for SSH and other ConPTY scenarios. In my opinion the console APIs are one thing and VT processing is another. Ideally, they should work together in the same application without conflicting, because that allows for seamless migrations to VT. But if there's a good reason for them to not work with each other, I think that's a necessary sacrifice to make. That's also the case here, because `DECSC` is the optimal way to reliably backup and restore attributes without assuming what the hosting terminal is capable of.
Author
Owner

@vefatica commented on GitHub (Dec 9, 2024):

@lhecker, is there any chance that access to the scroll-back (history) in WT will become as it is in a console? I miss it. I always liked the console model ... what you see is a window showing a portion of something bigger.

@vefatica commented on GitHub (Dec 9, 2024): @lhecker, is there any chance that access to the scroll-back (history) in WT will become as it is in a console? I miss it. I always liked the console model ... what you see is a window showing a portion of something bigger.
Author
Owner

@lhecker commented on GitHub (Dec 9, 2024):

I think that's unlikely for many reasons. We now support a scrollback way beyond 65535 rows for instance. There are also architectural reasons to not want it, because an immutable buffer can be way faster and more compact than a mutable one. Put differently, storing the scrollback separately from the viewport contents allows for some significant optimizations. But most importantly, being consistently restrictive ensures that people write applications for Windows that work well across SSH and other tools. Not being restrictive risks a "split" ecosystem.

@lhecker commented on GitHub (Dec 9, 2024): I think that's unlikely for many reasons. We now support a scrollback way beyond 65535 rows for instance. There are also architectural reasons to not want it, because an immutable buffer can be way faster and more compact than a mutable one. Put differently, storing the scrollback separately from the viewport contents allows for some significant optimizations. But most importantly, being consistently restrictive ensures that people write applications for Windows that work well across SSH and other tools. Not being restrictive risks a "split" ecosystem.
Author
Owner

@vefatica commented on GitHub (Dec 9, 2024):

It must have been a neat trick making it look like there's one continuous buffer.

@vefatica commented on GitHub (Dec 9, 2024): It must have been a neat trick making it look like there's one continuous buffer.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#22632