Commit Graph

8858 Commits

Author SHA1 Message Date
Mitchell Hashimoto
fdfc9fea2f input: send composed text in kitty keyboard protocol
When the kitty keyboard protocol "report all keys as escape codes" mode
was active, composed/IME text (e.g. from dead keys or compose sequences)
was silently dropped. 

This happened because the composed text is sent within our GTK apprt
with key=unidentified and no unshifted_codepoint, so no kitty entry was
found and the encoder returned without producing any output. The
plain-text fallback was also skipped because report_all bypasses it.

Send composed text as raw UTF-8 when no kitty entry is found, matching
the behavior of Kitty on Linux for me.

Fixes #10049
2026-03-03 08:57:24 -08:00
Mitchell Hashimoto
c8a0092301 fix: calculate cell size before presenting gtk window (#10459)
Fixes #7937

Added `computeInitialSize` to GTK `Surface` and call it in GTK
`Application` before the first `present()`, so the window manager
centers the correct size on initial show.

The issue occurs because the core `Surface.recomputeInitialSize()` runs
only after the renderer is initialized. In GTK, the `GLArea` isn’t
realized until after `present()`, so the initial size arrives too late
for WM centering.

**Limitations**: when we precompute size before `present()` we do not
have access to padding, so the sizing will be very slightly off... but
since it is only off a few pixels I was unable to tell visually that it
wasn't perfectly centered.

**Other thoughts**: I was hesitant to make changes to core `Surface`
because the issue is Linux-specific, but it may make sense to extract a
helper from `recomputeInitialSize` to avoid duplicating the sizing math.

**AI Disclosure:** I used AI to explore the project, help with any
language / API questions (I've never used zig before and rarely use
gtk), and make implementation suggestions.
2026-03-03 08:12:48 -08:00
Riccardo Mazzarini
4ce782b63f terminfo: add support for SGR dim
This PR implements the fix discussed in
https://github.com/ghostty-org/ghostty/discussions/11128
2026-03-03 09:48:48 +01:00
Mitchell Hashimoto
bb646926f8 config: respect cursor-click-to-move for OSC133 click to move
When cursor-click-to-move is set to false, disable all prompt
click-to-move mechanisms including shell-native methods such as OSC 133
cl= (arrow key synthesis) and click_events. 

I forgot to port this config over when we did the OSC133 stuff.

Also update the config documentation to accurately describe the current
behavior.

Fixes #11138
2026-03-02 20:15:49 -08:00
Mitchell Hashimoto
177612a4cf terminal: fix insertBlanks orphaned spacer_tail beyond right margin
When insertBlanks clears the entire region from cursor to the right
margin (scroll_amount == 0), a wide character whose head is at the right
margin gets cleared but its spacer_tail just beyond the margin is left
behind, causing a "spacer tail not following wide" page integrity
violation.

Move the right-margin wide-char cleanup from inside the scroll_amount >
0 block to before it, so it runs unconditionally — matching the
rowWillBeShifted pattern of cleaning up boundary-straddling wide chars
up front.

Found via AFL++ fuzzing. #11109
2026-03-02 11:24:54 -08:00
Mitchell Hashimoto
4768ffff73 terminal: fix insertLines/deleteLines orphaned cells on full clear (#11136)
When deleteLines or insertLines count >= scroll region height, all rows
go through the clear-only path (no shifting). This path did not call
rowWillBeShifted, leaving orphaned spacer_tail cells when wide
characters straddled the right margin boundary, causing a "spacer tail
not following wide" page integrity violation.

Add rowWillBeShifted before clearCells in the else branch of both
functions.

Found via AFL++ fuzzing. #11109
2026-03-02 11:02:14 -08:00
Mitchell Hashimoto
b39a00ddfa terminal: fix insertLines/deleteLines orphaned cells on full clear
When deleteLines or insertLines count >= scroll region height, all rows
go through the clear-only path (no shifting). This path did not call
rowWillBeShifted, leaving orphaned spacer_tail cells when wide characters
straddled the right margin boundary, causing a "spacer tail not following
wide" page integrity violation.

Add rowWillBeShifted before clearCells in the else branch of both
functions.

Found via AFL++ fuzzing. #11109
2026-03-02 10:57:41 -08:00
Mitchell Hashimoto
1ba9f9187e terminal: fix no-reflow resize leaving stale spacer heads
resizeWithoutReflowGrowCols has a fast path that reuses existing page
capacity when growing columns: it simply bumps page.size.cols without
touching cell data. If any row has a spacer_head at the old last column
(from a wide char that did not fit), that cell is no longer at the end
of the now-wider row, causing a page integrity violation.

Fix by checking for spacer_head cells at the old last column before
taking the fast path. If any are found, fall through to the slow path
which handles spacer heads correctly via cloneRowFrom.

Found by AFL++ stream fuzzer. #11109
2026-03-02 10:32:58 -08:00
Mitchell Hashimoto
e7030e73db terminal: fix printCell corrupting previous row when overwriting wide char
printCell, when overwriting a wide cell with a narrow cell at x<=1 and
y>0, unconditionally sets the last cell of the previous row to .narrow.
This is intended to clear a spacer_head left by a wrapped wide char, but
the cell could be a spacer_tail if a wide char fit entirely on the
previous row. Setting a spacer_tail to .narrow orphans the preceding
.wide cell, which later causes an integrity violation in insertBlanks
(assert that the cell after a .wide is .spacer_tail).

Fix by guarding the assignment so it only fires when the previous row's
last cell is actually a .spacer_head. The same fix is applied in both
the .wide and .spacer_tail branches of printCell.

Found by AFL++ stream fuzzer.
2026-03-02 07:28:12 -08:00
Mitchell Hashimoto
90e96a3891 terminal: fix insertBlanks integrity violation with wide char at right margin
insertBlanks checks whether the last source cell being shifted is wide
and clears it to avoid splitting, but it did not check the destination
cells at the right edge of the scroll region. When a wide character
straddles the right scroll margin (head at the margin, spacer_tail just
beyond it), the swap loop displaced the wide head without clearing the
orphaned spacer_tail, causing a page integrity violation
(InvalidSpacerTailLocation).

Fix by checking the cell at the right margin (last destination cell)
before the swap loop and clearing it along with its spacer_tail when it
is wide. 

Found by AFL++ stream fuzzer. #11109
2026-03-02 06:37:43 -08:00
Mitchell Hashimoto
97c11af347 terminal: fix integrity violation printing wide char with hyperlink at right edge
Printing a wide character at the right edge of the screen with an active
hyperlink triggered a page integrity violation (UnwrappedSpacerHead).
printCell wrote the spacer_head to the cell and then called
cursorSetHyperlink, whose internal integrity check observed the
spacer_head before printWrap had a chance to set the row wrap flag.

Fix by setting row.wrap = true before calling printCell for the
spacer_head case, so all integrity checks see a consistent state.
printWrap sets wrap again afterward, which is harmless. Found by AFL++
stream fuzzer.
2026-03-01 19:56:32 -08:00
Mitchell Hashimoto
2d69568a67 terminal: handle trailing colon in SGR underline parsing (#11113)
A trailing colon with no following sub-parameter (e.g. "ESC[58:4:m")
leaves the colon separator bit set on the last param without adding
another entry to the params array. When the SGR parser later iterates to
that param (4 = underline) and sees the colon bit, it entered the colon
path which asserted slice.len >= 2, but the slice only had one element.

Replace the assert with a bounds check that treats the malformed
sequence as a default single underline.

Add a regression test reproducing the crash from AFL++ fuzzing
(afl-out/stream/default/crashes/id:000021).

#11109
2026-03-01 15:38:28 -08:00
Mitchell Hashimoto
f253c54fac terminal: handle trailing colon in SGR underline parsing
A trailing colon with no following sub-parameter (e.g. "ESC[58:4:m")
leaves the colon separator bit set on the last param without adding
another entry to the params array. When the SGR parser later iterates
to that param (4 = underline) and sees the colon bit, it entered the
colon path which asserted slice.len >= 2, but the slice only had one
element.

Replace the assert with a bounds check that treats the malformed
sequence as a default single underline.

Add a regression test reproducing the crash from AFL++ fuzzing
(afl-out/stream/default/crashes/id:000021).
2026-03-01 15:28:56 -08:00
Mitchell Hashimoto
a595c00f3c terminal: fix panic on CSI g (TBC) with overflowing param
A fuzz crash found that CSI g with a parameter that saturates to
u16 max (65535) causes @enumFromInt to panic when narrowing to
TabClear (enum(u8)). Use std.meta.intToEnum instead, which safely
returns an error for out-of-range values.
2026-03-01 15:15:27 -08:00
Mitchell Hashimoto
e081a4abb4 fuzz/vt-stream 2026-03-01 14:55:07 -08:00
Mitchell Hashimoto
9157eb439a terminal: insertBlanks should not crash with count 0 and CSI @ clamps [1,)
CSI @ (ICH) with an explicit parameter of 0 should be clamped to 1,
matching xterm behavior. Previously, a zero count reached
Terminal.insertBlanks which called clearCells with an empty slice,
triggering an out-of-bounds panic.

Fix the stream dispatch to clamp 0 to 1 via @max, and add a defensive
guard in insertBlanks for count == 0. Found by AFL++ stream fuzzer.
2026-03-01 14:50:24 -08:00
Mitchell Hashimoto
dcaa8f3979 terminal: fix out-of-bounds access in CSI W handler with no params
CSI ? W (cursor tabulation control) accessed input.params[0] without
first checking that params.len > 0, causing an index out-of-bounds
panic when the sequence had an intermediate but no parameters.

Add a params.len == 1 guard before accessing params[0].

Found by AFL++ fuzzing.
2026-03-01 14:24:57 -08:00
Mitchell Hashimoto
a48cb630a8 libghostty-vt parser fuzzing, generic fuzz harness, using AFL++ (#11089)
This adds a `test/fuzz-libghostty` which is a standalone `zig build`
target for building an AFL++ instrumented executable for fuzzing the
libghostty-vt parser. I also added a `pkg/afl++` (based on zig-afl-kit)
so instrumenting objects and using AFL++ is a bit easier.

Fuzzing `libghostty-vt`'s parser is as easy as `zig build run`, but see
the README for a lot more details. I ran the fuzzer for ~14 hours total
and only found one crash #11088. I'm pretty confident at this point our
Parser layer isn't obviously crash-able, but need to instrument more
places to fuzz.

We don't use Zig's built-in fuzzing yet because as of 0.15 (our current
stable), it isn't ready and AFL++ is an industry proven tool to do this.
2026-03-01 13:16:52 -08:00
Mitchell Hashimoto
125b6e9f6c Clear key state overlay on "ignore" action (#11103)
This fixes a bug in the key state sequence overlay.

## Demo

In my ghostty config, I have

    keybind = ctrl+space>escape=ignore
    keybind = ctrl+space>p=toggle_command_palette
    ...

because I use `ctrl+space>` sequences for most things and so hitting
`esc` is my way to bail out of the sequence if I change my mind.

I just switched to tip and got the new GTK key sequence overlay. Here's
what I saw. In these screen recordings, the sequence of keys I press is

ctrl+space, escape, ctrl+space, escape, ctrl+space, escape, ctrl+space,
p


https://github.com/user-attachments/assets/4a37bc7e-b75c-4bd1-99de-f21f4211b5b5

after the fix:


https://github.com/user-attachments/assets/023be88e-1299-4219-920c-1b1134b2888c

## Notes

I believe this was also a leak, since the queued keys wouldn't be
deinited.

**AI usage:** Claude Code suggested the fix, then I read enough code to
convince myself that it makes sense.
2026-03-01 13:15:55 -08:00
Ulrich Drepper
851b62d738 🐛 Prevent git log output with signature information
When users have something like

[log]
        showSignature = true

in their .gitconfig files, invocations of the log or show git sub-command
emit additional information about signatures.  This additional output
disturbs the generation of short_hash in GitVersion.zig, the additional text
is copied verbatim into the string and then shown in the CSI >q output.

To fix it always suppress the output of the signature information.  This
has no effects when the setting is disabled anyway.
2026-03-01 10:51:48 +01:00
Caleb Spare
0ccaf3d5d6 Clear key state overlay on "ignore" action 2026-02-28 23:35:28 -08:00
Mitchell Hashimoto
adbb432930 test/fuzz-libghostty: basic afl++-based fuzzer for libghostty 2026-02-28 21:14:51 -08:00
Mitchell Hashimoto
12f43dfb7d fix(terminal): bounds check params in DCS passthrough entry
When a DCS sequence has more than MAX_PARAMS parameters, entering
dcs_passthrough would write to params[params_idx] without a bounds
check, causing an out-of-bounds access. Drop the entire DCS hook
when params overflow, consistent with how csi_dispatch handles it.

Found by AFL fuzzing.
2026-02-28 21:11:40 -08:00
Kat
de62821973 Rename ko.po back to ko_KR.po.
While it was renamed from ko_KR.UTF-8.po to ko.po in #10976, @uhojin,
a Korean locale maintainer, notes [1] that “ko_KR [*South* Korean] makes
more sense in locale context just to avoid any potential confusion
between 한국어 vs 조선어”.

Despite ko_KP (North Korean) not being present in glibc (as of version
2.43), and the ISO639 maintainers expressing disapproval of ko_KP [2],
it is possible opinions may change in the future, and individual
opinions may be contested—disambiguating doesn't hurt.

[1]: https://github.com/ghostty-org/ghostty/pull/10976#discussion_r2861424171
[2]: https://github.com/ghostty-org/ghostty/pull/10976#discussion_r2861359240
2026-02-28 19:00:35 +11:00
Kat
9192276d3e Rename pt.po back to pt_BR.po.
Portugal exists! Wikipedia notes [1] it to be the main other dialect.
There's already a PR for pt_PT support too:
https://github.com/ghostty-org/ghostty/pull/9078.

It was renamed from pt_BR.UTF-8.po to pt.po in #10976.

[1]: https://en.wikipedia.org/wiki/Portuguese_dialects
2026-02-28 17:43:08 +11:00
Mitchell Hashimoto
c78d9cba9e config: disable palette-generate by default
Following the discussion at #10852, I believe this is the right default.
I'm willing to continue to revisit this decisions, but Ghostty 1.3 is
around the corner and I don't think such a change like this should be
pushed into it.

I think palette generation is best left as a _theme author_ tool. A
Ghostty color theme could include `palette-generate=true` if it wants
to customize the 256-color palette more easily. Of course, end users can
as well anytime.

Another part of my reasoning is that TUI programs who want this behavior
can already achieve it themselves by mixing dark/light theme detection
via CSI 996 (https://contour-terminal.org/vt-extensions/color-palette-update-notifications/)
with OSC 4/10/11 color query and change sequences, both of which are
decently supported in the terminal ecosystem and fully supported in
Ghostty.

I'm also open to considering some kind of new sequence to make this
easier for TUIs (probably a mode) where they can opt-in to palette
generation plus "harmonius" palettes (see `palette-harmonius`) and
Ghostty does it on demand then. I think that'd solve the legacy vs new
TUI argument where legacy programs can continue to make assumptions
about the palette and new programs can opt-in to a more dynamic palette
without having to do a lot of work themselves.
2026-02-27 11:32:15 -08:00
Mitchell Hashimoto
3a21305d09 fix(macOS): filter phantom mouse events that defeat mouse-hide-while-typing (#11066)
## Summary

Ports the phantom mouse-motion position-equality check from the GTK
runtime to the embedded runtime (used by macOS).

On macOS, TUI apps like Zellij that frequently update the window title
cause phantom `mouseMoved` events at the same coordinates. These flow
through `embedded.zig` → `Surface.zig` `cursorPosCallback` →
`showMouse()`, which explicitly calls
`NSCursor.setHiddenUntilMouseMoves(false)` and unhides the cursor,
defeating `mouse-hide-while-typing`.

The GTK runtime already filters these in PR #4973 (for #3345):

```zig
const is_cursor_still = @abs(priv.cursor_pos.x - pos.x) < 1 and
    @abs(priv.cursor_pos.y - pos.y) < 1;
if (is_cursor_still) return;
```

This PR adds the same check to `embedded.zig`'s `cursorPosCallback`,
using the already-stored `self.cursor_pos` field.

## Test plan

- [x] Enable `mouse-hide-while-typing = true` in Ghostty config
- [ ] Run a TUI app that updates the window title frequently (e.g.
Zellij)
- [ ] Type — cursor should hide and stay hidden despite title updates
- [ ] Move the mouse — cursor should reappear normally
- [ ] Verify no regressions with normal mouse movement,
focus-follows-mouse, or link hovering
2026-02-27 11:25:18 -08:00
Mitchell Hashimoto
0bdd3bb6b5 feat(vt): Parse UAPI OSC 3008 hierarchical context signalling (#11057)
Implements parsing for OSC 3008, which allows terminal emulators to keep
track of the stack of processes that have current control over the tty.
The implementation mirrors existing `semantic_prompt.zig` architecture
and natively maps UAPI definitions to Zig structures with lazy
evaluation for optional metadata.

Fixes #10900
2026-02-27 10:46:35 -08:00
Prakhar54-byte
eafdbaaada refactor: simplify Enum parse call and loop parsing logic 2026-02-27 22:58:08 +05:30
linustalacko
2a41401463 fix(macOS): filter phantom mouse events that defeat mouse-hide-while-typing
On macOS, TUI apps like Zellij that frequently update the window title
cause phantom mouse-move events to be generated at the same coordinates.
These phantom events reach cursorPosCallback in the core, which calls
showMouse() and explicitly unhides the cursor via
NSCursor.setHiddenUntilMouseMoves(false), defeating the
mouse-hide-while-typing feature.

This ports the same position-equality check already present in the GTK
runtime (added in PR #4973 for issue #3345) to the embedded runtime used
by macOS. If the cursor position hasn't changed by more than 1px, the
event is discarded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 09:08:13 -08:00
Prakhar54-byte
3e1004717b refactor: apply PR feedback
- Use `std.meta.stringToEnum` in ContextType and ExitStatus
- Ensure `parseInt` only accepts digits for pids
- Use `@tagName` for string representation in Field
- Rename `fields_raw` to `metadata`
- Rename `readField` to `readOption`
2026-02-27 22:20:42 +05:30
Jeffrey C. Ollie
cdf0dd15e9 testing: use std.Build.TranslateC instead of @cImport 2026-02-27 10:13:03 -06:00
Prakhar54-byte
9da6588c16 feat(vt): Parse UAPI OSC 3008 hierarchical context signalling
Implements parsing for OSC 3008, which allows terminal emulators to keep track of the stack of processes that have current control over the tty. The implementation mirrors existing `semantic_prompt.zig` architecture and natively maps UAPI definitions to Zig structures with lazy evaluation for optional metadata.

Fixes #10900
2026-02-27 21:10:42 +05:30
Jeffrey C. Ollie
ea5b07d20f core: add tests for ghostty.h
* ensure that `ghostty.h` compiles during basic Zig tests
* ensure that non-exhaustive enums are kept synchronized between
  `ghostty.h` and their respective Zig counterpart.
* adjust some enums that varied from established conventions
2026-02-27 09:22:23 -06:00
Mitchell Hashimoto
f19d847f95 core: parse cmdline and cmdline_url semantic prompt options (#11046) 2026-02-26 19:36:38 -08:00
Kat
d5f621044d Update language config option's documentation.
Follow-up to #10976.
2026-02-27 11:08:19 +11:00
Jeffrey C. Ollie
3b4e2bbcbe core: parse cmdline and cmdline_url semantic prompt options 2026-02-26 17:52:09 -06:00
Mitchell Hashimoto
ca09c0ef2e macOS: add "command finished" notifications (#10934)
fixes https://github.com/ghostty-org/ghostty/issues/10840

Implement command finished notifications for MacOS. Building on the work
of #8992

### AI Tools Used
* Cursor
* Models
    * Opus 4.6
    * Composer 1.5
2026-02-26 13:47:24 -08:00
Jeffrey C. Ollie
62873f60c5 i18n: rename .po files (#10976)
This seems to be the defacto standard for naming `.po` files. See the
GTK source code [1] as an example. I was unable to find any definitive
documentation on the naming.

Replaces: #10905

[1] https://gitlab.gnome.org/GNOME/gtk/-/tree/main/po?ref_type=heads
2026-02-26 15:23:27 -06:00
Mitchell Hashimoto
39d163fee2 zsh: fix ssh-terminfo shell integration to not interpret escape characters (#11038)
With zsh, when installing the ghostty terminfo on a server via the
ssh-terminfo shell integration, parts of the terminfo get mangled. In
particular, the newline escape sequence in
```
> infocmp -0 -x xterm-ghostty | grep ind=
 ...,ind=\n,indn=...
 ```
gets interpreted by `print` as a literal newline, which then just gets ignored / does not have the intended effect.

Documentation for the `-r` flag of `print` used in the fix is [here](https://zsh.sourceforge.io/Doc/Release/Shell-Builtin-Commands.html#:~:text=Ignore%20the%20escape%20conventions%20of%20echo.).

### Testing locally
You can directly demonstrate this locally. This outputs a host of warning messages:
```
ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null)
print "$ssh_terminfo" | tic -x -
```
Whereas
```print -r "$ssh_terminfo" | tic -x -```
or
```infocmp -0 -x xterm-ghostty | tic -x -```
work without issue.

### Testing remotely
The most visible way is to observe the output of `htop` before and after the change.

More directly, the output of `infocmp -x xterm-ghostty | grep " ind="` should be
```ich=\E[%p1%d@, ich1=\E[@, il=\E[%p1%dL, il1=\E[L, ind=\n,```
instead of 
```ich=\E[%p1%d@, ich1=\E[@, il=\E[%p1%dL, il1=\E[L, ind=,```

---

Discussed in #11031.

---
AI disclosure: I used Claude for parts of figuring out what was going on. The fix itself and the rest was written and tested by myself.
2026-02-26 13:17:24 -08:00
Joseph Martinsen
3b5a7b77d3 macos: implement notify on command finish 2026-02-26 13:12:29 -08:00
Jeffrey C. Ollie
45d1787eff i18n: rename .po files
This seems to be the defacto standard for naming `.po` files. See the
GTK source code [1] as an example. I was unable to find any definitive
documentation on the naming.

Replaces: #10905

[1] https://gitlab.gnome.org/GNOME/gtk/-/tree/main/po?ref_type=heads
2026-02-26 15:10:08 -06:00
Mitchell Hashimoto
dc514c9e11 build: don't build OpenGL support into imgui on iOS 2026-02-26 10:31:21 -08:00
Mitchell Hashimoto
9fe3cc125d apprt/gtk: use new get effective title 2026-02-26 07:19:26 -08:00
Noah Bernsohn
f38234bc5b apprt: show title override in command palette jump commands 2026-02-26 07:18:05 -08:00
Michael Engelhard
875985dbd7 zsh: fix ssh-terminfo shell integration to not interpret escape characters 2026-02-26 13:06:07 +01:00
markhuot
20351a71d0 Fix Palette.C array size typo: [265] → [256]
The C struct Palette.C declared colors as [265]Color.C, but the
terminal palette is 256 colors (terminal.color.Palette = [256]RGB)
and the C header ghostty_config_palette_s correctly uses colors[256].

The mismatch causes ghostty_config_get to write 265×3 = 795 bytes
through a pointer sized for 256×3 = 768 bytes, producing a 27-byte
buffer overflow. On macOS Release builds with stack protector enabled,
this triggers __stack_chk_fail → SIGABRT on launch.
2026-02-25 20:17:13 -05:00
Mitchell Hashimoto
7db8346fca apprt/gtk: fix SIGSEGV on ImGui GLArea re-realize
Fixes #10406

ImGui_ImplOpenGL3_Shutdown() calls imgl3wShutdown() which dlcloses the
GL library handles but does not zero out the imgl3w function pointer
table (imgl3wProcs). When a GLArea is re-realized (e.g. during
reparenting), ImGui_ImplOpenGL3_Init() calls ImGui_ImplOpenGL3_InitLoader()
which checks "if (glGetIntegerv == nullptr)". Since the stale pointers
are non-null, it skips re-initialization. The next GL call through a
dangling function pointer causes a SIGSEGV.

Fix this by introducing ImGui_ImplOpenGL3_ShutdownWithLoaderCleanup()
which calls the normal shutdown and then zeroes the imgl3wProcs table,
forcing the next Init to reload GL function pointers via imgl3wInit().

Also properly destroy the ImGui context and reset widget state in
glAreaUnrealize so re-realize starts clean. This was extra but was
probably leaking memory.
2026-02-25 15:25:22 -08:00
Jeffrey C. Ollie
57d570525b gtk: clean up title renaming and fix a small leak 2026-02-24 10:28:33 -06:00
ClearAspect
7a4bddd37b renderer: added cursor style and visibility uniforms
Specifically:
iCurrentCursorStyle
iPreviousCursorStyle
iCurrentCursorVisible
iPreviousCursorVisible

Visibility calculated and updated independently from the typical cursor
unifrom updates to preserve cursor style even when not in the viewport
or set to be hidden
2026-02-23 14:11:36 -08:00