Add new `selection-word-chars` config option to customize which characters
mark word boundaries during text selection operations (double-click, word
selection, etc.). Similar to zsh's WORDCHARS environment variable, but
specifies boundary characters rather than word characters.
Default boundaries: ` \t'"│`|:;,()[]{}<>$`
Users can now customize word selection behavior, such as treating
semicolons as part of words or excluding periods from boundaries:
selection-word-chars = " \t'\"│`|:,()[]{}<>$"
Changes:
- Add selection-word-chars config field with comprehensive documentation
- Modify selectWord() and selectWordBetween() to accept boundary_chars parameter
- Parse UTF-8 boundary string to u32 codepoints at runtime
- Update all call sites in Surface.zig and embedded.zig
- Update all test cases to pass boundary characters
We need to have sane behavior in error handling because the running
program that sends the restore cursor command has no way to realize it
failed. So if our style fails to add (our only fail case) then we revert
to no style.
https://ampcode.com/threads/T-019bd7dc-cf0b-7439-ad2f-218b3406277a
Fixes#10282
The function `cursorChangePin` is supposed to be called anytime the
cursor page pin changes, but it itself may alter the page pin if setting
up the underlying managed memory forces a page size adjustment.
Multiple callers to this function were erroneously reusing the old page
pin value.
This improves the `clearCells` function since it only has to update once
after clearing all of the individual cells, or not at all if the whole
row was cleared since then it knows for sure that it cleared them all.
This also makes it so that the row style flag is properly tracked when
cells are cleared but not the whole row.
This could cause a 0-length hyperlink to be present in the screen,
which, in ReleaseFast, causes a lockup as the string alloc tries to
iterate `1..0` to allocate 0 chunks.
It was previously possible for `eraseRow` to move the cursor pin to a
different page, and then the call to `cursorChangePin` would try to free
the cursor style from that page even though that's not the page it
belongs to, which creates memory corruption in release modes and
integrity violations or assertions in debug mode.
As a bonus, this should actually be faster this way than the old code,
since it avoids needless work that `cursorChangePin` otherwise does.
This removes all existing functionality that I know of that encodes a
terminal, screen, pagelist, or page as plaintext and unifies all logic
onto the formatter system.
This replaces the logic of Screen.selectionString with calls to
ScreenFormatter.
This means that all our various selection-based features like copying to
clipboards now uses the new formatter. The formatter code is now
user-facing.
This forced us to pass all selectionString tests which revealed some
edge cases that were not handled correctly before in the formatter! The
formatter now handles:
- Plain text now emits `\n` instead of `\r\n`. VT emits `\r\n`
- Rectangular selections
- Various wide character edge cases
- Selection is now inclusive on the end, not exclusive
A whole bunch of inline annotations, some of these were tracked down
with Instruments.app, others are guesses / just seemed right because
they were trivial wrapper functions.
Regardless, these changes are ultimately supported by improved vtebench
results on my machine (Apple M3 Max).
Powerline glyphs were treated as whitespace, giving the preceding cell a
constraint width of 2 and cutting off icons in people's prompts and
statuslines. It is however correct to not treat Powerline glyphs like
other Nerd Font symbols; they should simply be treated as normal
characters, just like their relatives in the block elements unicode
block.
This resolves
https://discord.com/channels/1005603569187160125/1417236683266592798
(never promoted to an issue, but real and easy to reproduce).
**Tip**
<img width="215" height="63" alt="Screenshot 2025-09-21 at 16 57 58"
src="https://github.com/user-attachments/assets/81e770c5-d688-4d8e-839c-1f4288703c06"
/>
**This PR**
<img width="215" height="63" alt="Screenshot 2025-09-21 at 16 58 42"
src="https://github.com/user-attachments/assets/5d2dd770-0314-46f6-99b5-237a0933998e"
/>
The constraint width logic was untested but contains some quite subtle
interactions, so I wrote a suite of tests covering the cases I'm aware
of.
While working on this code I also resolved a TODO comment to add all the
box drawing/block element type characters to the set of codepoints
excluded from the minimum contrast settings.
This test-only function wraps testWriteString with semantic prompt
marking. This replaces the manual, row-based semantic_prompt field
manipulation we were doing in all of our prompt-related test setups.
This function's heuristics are a little complex because it wraps
testWriteString as a "black box"; we don't benefit from that function's
own line-based logic to know which rows need to be updated with the
semantic prompt flag. We need to infer them externally instead.
I considered adding an options argument to testWriteString that would
allow passing e.g. a semantic_prompt prompt. Given that it's called from
200+ places, that would involve a lot of unrelated changes, but it
remains an "option" (ha!) if there's value there for other cases.
I also have plans that move us from row-based to cell-based semantic
tracking, where the current semantic type is tracked by the cursor. In
that implementation, testWriteString can update the written cells
directly, and testWriteSemanticString just helps manage the cursor's
state. Introducing testWriteSemanticString here and now therefore helps
bridge us to that world while maintaining test consistency.
These tests write specific lines into a 10-column-wide test screen. The
"prompt3$ input3\n" writes exceed that column limit, and some of their
characters wrap onto the following line.
These tests' current assertions aren't sensitive to that overflow, but
I spotted the problem while doing some related work, and I thought it
worth making these corrections to avoid any future surprises.
Cleans up the logic, checks for out of bounds using rows instead of
sel.contains because that excludes cases where a rectangle selection
doesn't include the leftmost column.
Also adds test for clipping behavior of rectangular selections.
Fixes an issue where rectangle selections would appear visually wrong if
their start or end were out of the viewport area, because when cloning
them the restored pins were defaulting to the start and end of the row
instead of the appropriate column.
This commit changes a LOT of areas of the code to use decl literals
instead of redundantly referring to the type.
These changes were mostly driven by some regex searches and then manual
adjustment on a case-by-case basis.
I almost certainly missed quite a few places where decl literals could
be used, but this is a good first step in converting things, and other
instances can be addressed when they're discovered.
I tested GLFW+Metal and building the framework on macOS and tested a GTK
build on Linux, so I'm 99% sure I didn't introduce any syntax errors or
other problems with this. (fingers crossed)
Fixes#7066
This fixes an issue where under certain conditions (expanded below), we
would not clear the correct row, leading to the screen having duplicate
data.
This was triggered by a page state of the following:
```
+----------+ = PAGE 0
... : :
4305 |1ABCD00000|
4306 |2EFGH00000|
:^ : = PIN 0
+-------------+ ACTIVE
4307 |3IJKL00000| | 0
+----------+ :
+----------+ : = PAGE 1
0 | | | 1
1 | | | 2
+----------+ :
+-------------+
```
Namely, the cursor had to NOT be on the last row of the first page,
but somewhere on the first page. Then, when an `index` (LF) operation
was performed the result would look like this:
```
+----------+ = PAGE 0
... : :
4305 |1ABCD00000|
4306 |2EFGH00000|
+-------------+ ACTIVE
4307 |3IJKL00000| | 0
:^ : : = PIN 0
+----------+ :
+----------+ : = PAGE 1
0 |3IJKL00000| | 1
1 | | | 2
+----------+ :
+-------------+
```
The `3IJKL` line was duplicated. What was happening here is that we
performed the index operation correctly but failed to clear the cursor
line as expected.
This is because we were always clearing the first row in the page
instead of the row of the cursor.
Test added.
clearCells() always asserts its page's integrity after finishing its
work (via a `defer`). We don't need to re-assert the page's integrity
immediately thereafter.
Fixes#5718
When a terminal is resized with text reflow (i.e. soft-wrapped text), the cursor
is generally reflowed with it.
For example, imagine a terminal window 5-columns wide and you type the
following without pressing enter. The cursor is on the X.
```
OOOOO
OOX
```
If you resize the window now to 8 or more columns, this happens, as expected:
```
OOOOOOOX
```
As expected, the cursor remains on the "X". This behaves like any other text
input...
Terminals also provide an escape sequence to
[save the cursor (ESC 7 aka DECSC)](https://ghostty.org/docs/vt/esc/decsc).
This includes, amongst other things, the cursor position. The cursor can be
restored with [DECRC](https://ghostty.org/docs/vt/esc/decrc).
The behavior of the position of the _saved cursor_ in the context of text
reflow is unspecified and varies wildly between terminals Ghostty does this
right now (as do many other terminals):
```
OOOOOOOO
X
```
This commit changes the behavior so that we reflow the saved cursor.
Fully reset the kitty image storage instead of using the delete handler,
previously this caused a memory corruption / likely segfault due to use
of undefined, since the delete handler tries to clear the tracked pins
for placements, which it gets from the terminal, for which `undefined`
was passed in before.
Previously assumed adjusted capacities would always fit the cursor style
and hyperlink, this is not necessarily the case and led to memory
corruption in scenarios with large hyperlinks.