Fixes#906
This changes our resize behavior when increasing row height.
If the cursor was originally at the bottom of the viewport, existing
scrollback (if it exists) will be "pulled down" from the top,
effectively keeping the cursor at the bottom. This is the behavior
today, prior to this commit.
If the cursor is not at the bottom of the viewport, scrollback will NOT
be "pulled down" and instead blank lines will be added _below_. This is
new behavior.
Fixes#906
Previously, when the cursor isn't at the bottom and you resized to less
cols, the cursor would jump to the bottom of the viewport. But if you
resized to more columns it didn't do this. This was jarring. This commit
attempts to keep the cursor at the same place.
Fixes#741
This completely reimplements double-click-and-drag logic for selecting
by word. The previous implementation was horribly broken. See #741 for
all the details.
The implemented logic now is:
* A double-click initiates a select-by-word selection mechanism.
- A double-click may start on a word or whitespace
- If the initial double-click is on a word, that word is immediately selected.
- If the initial double-click is on whitespace, the whitespace is not selected.
* A "word" is determined by a non-boundary character meeting a boundary character.
- A boundary character is `NUL` ` ` (space) `\t` `'` `"`
- This list is somewhat arbitrary to make the terminal "feel" good.
- Cell SGR states (fg/bg, bold, italic, etc.) have no effect on boundary determination or selection logic.
* As the user drags _on the same line_:
- No selection change occurs until the cursor is over a new word. Whitespace change does nothing.
- When selection is over a new word, that entire word added to the selection.
* When the user drags _up_ one or more lines:
- If the cursor is over whitespace, all lines from the selection point up to but not including the cursor line are selected.
* This selection is done in accordance to the previous rules.
- If the cursor is over a word, the word becomes the beginning of the selection.
- The end of the selection in all cases is the first word at or before the initial double-click point.
* When the user drags _down_ one or more lines:
- The same logic as _up_ but swap the "beginning" and "end" of selection terminology.
* With this logic, the behavior of Ghostty has the following invariants:
- Whitespace is never selected unless it is between two selected words
- Selection implies at least one word is highlighted
- The initial double-click point marks the beginning or end of a selection, never the middle.
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.
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
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
When font shaping grapheme clusters, we erroneously used the font index
of a font that only matches the first codepoint in the cell. This led to the
combining characters being [usually] unknown and rendering as boxes.
For a grapheme, we must find a font face that has a glyph for _all codepoints_
in the grapheme.
This also fixes an issue where we now properly render the unicode replacement
character if we can't find a font satisfying a codepoint.