Fixes#315
This function has various cases that can be hit depending on the state
of the underlying circular buffer. It is a very well tested function but
this particular branch wasn't tested and unsurprisingly turns out there
is a bug in it.
Consider the following circular buffer state representing a terminal
screen with 1 column, 5 rows. Assume the circular buffer representing
this screen is such that `head == tail` and `head = 4` (zero-indexed,
full buffer size is 5). The head and tail are shown with an arrow below.
┌───────────────────────────────────────────────────────────────┐
│ B │
├───────────────────────────────────────────────────────────────┤
│ C │
├───────────────────────────────────────────────────────────────┤
│ D │
├───────────────────────────────────────────────────────────────┤
│ E │
├───────────────────────────────────────────────────────────────┤ ◀─── Head
│ A │ Tail
└───────────────────────────────────────────────────────────────┘
The screen contents are "A B C D E" with each character on a new line.
Next, we set a scroll region from y=0 to y=3 (len=4). The scroll region
contents are "A B C D". Next, we issue a "delete lines" command with
n=2 (`CSI 2 M`) while the cursor is at the top-left corner. The delete
lines command deletes the given number of lines (n=2 in this case) and
shifts the remaining lines up. It does this only within the context
of the scroll region. Therefore, for `CSI 2 M` we would expect our
screen to become "C D _ _ E" (A, B are deleted, C, D shift up, and
E is untouched because its outside of the scroll region).
When executing this operation, we request the memory regions containing
the scroll region. This results in two pointers and lengths. For our
circular buffer state, we get the following:
┌───────────────────────────────────────────────────────────────┐ ◀──── ptr0
│ A │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐ ◀──── ptr1
│ B │
├───────────────────────────────────────────────────────────────┤
│ C │
├───────────────────────────────────────────────────────────────┤
│ D │
└───────────────────────────────────────────────────────────────┘
We get two pointers because our circular buffer wraps around the bottom.
The diagram above shows them in top/bottom order not in memory order
(The value of `ptr0 > ptr1` but it doesn't matter for the bug).
The way the math works is as follows:
1. We calculate the number of lines we need to shift up. That
value is `height - n`. Our height is 4 (scroll region height) and
our n is 2 (`CSI 2 M`), so we know we're shifting up 2 lines.
Let's call this `shift_lines`.
2. Our start copy offset is `n` because the lines we are retaining
are exactly after the `n` we're deleting (i.e. we're deleting 2
lines so the start of the lines we're shifting up is the 3rd line).
Let's call this `start_offset`.
3. We realize that our start offset is greater than the size of ptr0,
so we must be copy from ptr1 into ptr0. Further, we know our start
offset into ptr1 must be `start_offset - ptr0.len`.
Let's call this `start_offset_ptr1 = 1`.
4. Copy `ptr1[start_offset_ptr1]` to `ptr0`. We copy up to
`shift_lines` amount. `shift_lines` is 2 but `ptr0.len` is only
`1`. So, we actually copy `@min(shift_lines, ptr0.len)` and have
`1` line remaining.
Let's call that `remaining = 1`.
5. Copy `remaining` from `ptr1[ptr0.len]` to `ptr1`.
6. Next we need to zero our remaining lines. Our slices only contain
our scroll region so we know we can zero the memory from
`ptr1[remaining]` to the end of `ptr1`. We know this because step 5
only copied `remaining` bytes.
The end result looks like this:
┌───────────────────────────────────────────────────────────────┐ ◀──── ptr[0]
│ C │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐ ◀──── ptr[1]
│ D │
├───────────────────────────────────────────────────────────────┤
│ │
├───────────────────────────────────────────────────────────────┤
│ │
└───────────────────────────────────────────────────────────────┘
The bug was in step 6. We were incorrectly zeroing from `start_offset_ptr1`
instead of `remaining`. This was just a simple typo. The results are
devastating, but only under the exactly correct circumstances (those
in this commit message). In that scenario, the bug produced the
following:
┌───────────────────────────────────────────────────────────────┐ ◀────────── ptr[0]
│ C │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐ ◀────────── ptr[1]
│ D │
├───────────────────────────────────────────────────────────────┤
│ C │
├───────────────────────────────────────────────────────────────┤
│ │
└───────────────────────────────────────────────────────────────┘
Notice the incorrect "C" that remains and is not zeroed.
This example showed a scenario with 1 column and leaving only 1 line out
of the scroll region. The real bug in #315 would often mistakingly leave
multiple lines in the scroll region. The effect was that scrolling
produced garbage lines because you'd "scroll" and part of what should've
scrolled would remain.
This garbage state was only in the terminal screen state, so it didn't
impact actual programs running or their data (i.e. vim). But, it made
the program unusable.
Any row created from scrolling via IND ("\x1BD") should have it's
background set as the current background. This can be verified in any
terminal with
$ echo -e "\x1B[41m" && cat -v"
Followed by pressing enter to scroll the screen. We expect to see red
rows appear. Add test case to verify.
Fixes: alacritty/vim_large_screen_scroll
When scrolling up or deleting lines (effectively the same operation),
the inserted lines should only inherit the bg attribute of the cursor.
This behavior is similar to erase display and erase line. Update tests
to reflect this behavior.
Erasing the display should be done with only the background attribute of
the current pen. This is the current behavior but is done by altering
the current pen state. The pen state should remain after erasure. Use a
new pen variable to erase the display to enable retaining the pen state.
Add a test condition.
Each screen (primary and alternate) retains the state of the current
charset when DECSC (save cursor) is called. Move the CharsetState into
the screen to enable saving the state with each screen.
Add a test for charset state on screen change
The name sounds obvious, but nothing ever is with ANSI escapes. ICH
(Insert Blank Characters) should insert a cell with no attributes or
colors set.
Remove comment about inserting a space for tests: the tests pass with a
blank cell inserted.
Remove comment about inserted cells having current SGR.
Update tests to check for attribute of inserted cell.
When DCH (deleteChars) deletes cells from a line, cells are inserted
from the right. The new cells should not have the current pen style, and
should instead be an empty cell.
Add check for this in the existing test.
Fixes: alacritty/deccolm_reset
Add a check for 'has_bg', and if it is set retain the background color.
If it isn't set, we are safe to set the pen to it's default.
Fixes: alacritty/colored_reset
When erasing the display, all attributes of the pen must be cleared
_except_ for the background. Add unit tests for erasing the display in
all scenarios.
Fixes: alacritty/clear_underline
fullReset resets the state of the terminal. This method calls
eraseDisplay, which depends on the state of the cursor pen for setting
the cell styles when it erases. Reset the state of the cursor prior to
calling eraseDisplay to ensure a clean reset.
Fixes: alacritty/alt_reset test
The osc_string state of the parser limited accepted bytes to 0x7F. When
parsing a utf-8 encoded string as part of an OSC string, the parser
would encounter an error and abort the OSC parsing, allowing any
remaining bytes to be leaked (possibly) as printable characters to the
terminal window.
Allow any byte in the range 0x20 - 0xFF to be accepted by osc_put. Add
test cases which conflict with the 'anywhere' transitions (IE the utf8
sequence includes C1 control codes which might transition to another
state).
XTVERSION (CSI > 0 q) is used by some libraries to identify the terminal
+ version. Respond to this query with `ghostty {version_string}`. There
is no formal format for this response. A roundup of a few tested
terminals show two primary formats. This patch opts to save one byte and
use the `name SP version` semantics.
foot: foot(version)
xterm: XTerm(version)
contour: contour version
wezterm: wezterm version
Reference: https://github.com/dankamongmen/notcurses/blob/master/TERMINALS.md#notes-for-terminal-authors
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
* Change state names to more human readable query_default_fg/bg
* Single-line state prongs
* String terminator is not an enum
* Removed `endWithStringTerminator` and added nullabe arg to `end`
* Fixed a color reporting bug, fg/bg wasn't correctly picked
These OSC commands report the default foreground and background colors.
Most terminals return the RGB components scaled up to 16-bit components, because some
legacy software are unable to read 8-bit components. The PR follows this conventions.
iTerm2 allow 8-bit reporting through a config option, and a similar option is
added here. In addition to picking between scaled and unscaled reporting, the user
can also turn off OSC 10/11 replies altogether.
Scaling is essentially c / 1 * 65535, where c is the 8-bit component, and reporting
is left-padded with zeros if necessary. This format appears to stem from the XParseColor
format.
This makes a few major changes:
- cursor style on terminal is single source of stylistic truth
- cursor style is split between style and style request
- cursor blinking is handled by the renderer thread
- cursor style/visibility is no longer stored as persistent state on
renderers
- cursor style computation is extracted to be shared by all renderers
- mode 12 "cursor_blinking" is now source of truth on whether blinking
is enabled or not
- CSI q and mode 12 are synced like xterm