From 5d08537ea4f9a6e3a31158a48783b6d53f79a9df Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 08:00:07 +0000 Subject: [PATCH] Bump github.com/gdamore/tcell/v3 from 3.3.0 to 3.4.0 Bumps [github.com/gdamore/tcell/v3](https://github.com/gdamore/tcell) from 3.3.0 to 3.4.0. - [Release notes](https://github.com/gdamore/tcell/releases) - [Changelog](https://github.com/gdamore/tcell/blob/main/CHANGESv3.md) - [Commits](https://github.com/gdamore/tcell/compare/v3.3.0...v3.4.0) --- updated-dependencies: - dependency-name: github.com/gdamore/tcell/v3 dependency-version: 3.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +- go.sum | 12 +- .../github.com/gdamore/tcell/v3/CHANGESv3.md | 4 + .../gdamore/tcell/v3/README-wasm.md | 37 +- vendor/github.com/gdamore/tcell/v3/README.md | 18 + vendor/github.com/gdamore/tcell/v3/cell.go | 26 +- vendor/github.com/gdamore/tcell/v3/input.go | 475 +++++++++--- vendor/github.com/gdamore/tcell/v3/key.go | 270 ++++++- vendor/github.com/gdamore/tcell/v3/mouse.go | 5 +- vendor/github.com/gdamore/tcell/v3/screen.go | 25 +- vendor/github.com/gdamore/tcell/v3/style.go | 35 +- vendor/github.com/gdamore/tcell/v3/tscreen.go | 504 +++++++++--- .../gdamore/tcell/v3/tty/tty_win.go | 29 +- .../github.com/gdamore/tcell/v3/tty/utf16.go | 47 ++ .../github.com/gdamore/tcell/v3/vt/emulate.go | 97 ++- vendor/github.com/gdamore/tcell/v3/vt/key.go | 13 +- vendor/github.com/gdamore/tcell/v3/vt/mock.go | 11 +- vendor/github.com/gdamore/tcell/v3/wscreen.go | 729 ++++-------------- vendor/modules.txt | 6 +- 19 files changed, 1508 insertions(+), 841 deletions(-) create mode 100644 vendor/github.com/gdamore/tcell/v3/tty/utf16.go diff --git a/go.mod b/go.mod index b8732b004..52e7da2d5 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/cli/go-gh/v2 v2.13.0 github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21 github.com/creack/pty v1.1.24 - github.com/gdamore/tcell/v3 v3.3.0 + github.com/gdamore/tcell/v3 v3.4.0 github.com/go-errors/errors v1.5.1 github.com/gookit/color v1.6.1 github.com/integrii/flaggy v1.8.0 @@ -68,8 +68,8 @@ require ( github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/term v0.42.0 // indirect - golang.org/x/text v0.36.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/go.sum b/go.sum index f41968456..e92e532e2 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= -github.com/gdamore/tcell/v3 v3.3.0 h1:lGo3VwiV8iYMH4TdwBDwEOoDctnOLns6QfrXvZkKNe8= -github.com/gdamore/tcell/v3 v3.3.0/go.mod h1:8CJpEjUiAlFIrs7jUhzobiZsSLBAtXo8Dq9mCTrVnqo= +github.com/gdamore/tcell/v3 v3.4.0 h1:VUym1HQZiYodA5PGQrqLxF7QwqQndcAUwQD7G7XUy5E= +github.com/gdamore/tcell/v3 v3.4.0/go.mod h1:fjKxNiIFwbzTxDU+i+AAMz+xPOgXVaZq5tbShsKseHc= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -167,15 +167,15 @@ golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/vendor/github.com/gdamore/tcell/v3/CHANGESv3.md b/vendor/github.com/gdamore/tcell/v3/CHANGESv3.md index 78e8dbadd..26a0c2e57 100644 --- a/vendor/github.com/gdamore/tcell/v3/CHANGESv3.md +++ b/vendor/github.com/gdamore/tcell/v3/CHANGESv3.md @@ -45,6 +45,10 @@ the associated lower case rune (e.g. "a", "b", etc.) and `ModCtrl`. The `KeyBackspace2` key is no longer delivered, but is converted to `KeyBackspace`. (This resolves some inconsistency around e.g. CTRL-H vs DELETE.) +When advanced key reporting is enabled, Shift-Tab is reported as `KeyTab` with +`ModShift`, not as `KeyBacktab`. Legacy key reporting still reports Shift-Tab +as `KeyBacktab`. + ### Termbox Compatibility Removed The `termbox` compatibility package is removed. Few applications were using it, diff --git a/vendor/github.com/gdamore/tcell/v3/README-wasm.md b/vendor/github.com/gdamore/tcell/v3/README-wasm.md index 09591095e..4e29a3dac 100644 --- a/vendor/github.com/gdamore/tcell/v3/README-wasm.md +++ b/vendor/github.com/gdamore/tcell/v3/README-wasm.md @@ -11,11 +11,30 @@ GOOS=js GOARCH=wasm go build -o yourfile.wasm ## Additional files -You also need 5 other files in the same directory as the wasm. Four (`tcell.html`, `tcell.js`, `termstyle.css`, and `beep.wav`) are provided in the `webfiles` directory. The last one, `wasm_exec.js`, can be copied from GOROOT into the current directory by executing +You also need the supporting web files in the same directory as the wasm. The files `tcell.html`, `tcell.js`, `termstyle.css`, and `beep.wav`, plus the `ghostty-web` directory, are provided in the `webfiles` directory. The last file, `wasm_exec.js`, can be copied from GOROOT into the current directory by executing ```sh cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" ./ ``` +The web frontend uses `ghostty-web`. The required browser runtime files are vendored in `webfiles/ghostty-web` and must be copied alongside `tcell.js`; no npm, bundler, or external CDN is required. The vendored `ghostty-web` files are MIT licensed; see `webfiles/ghostty-web/LICENSE`. + +```sh +cp -R webfiles/ghostty-web /path/to/dir/to/serve/ +``` + +The vendored `ghostty-web.js` is intentionally browser-only. Its upstream Node `readFile` fallback import is removed so browser-oriented servers and bundlers such as Vite do not try to resolve a Node file-system shim; the bundled code loads `ghostty-vt.wasm` with `fetch`. + +For example: + +```sh +mkdir -p /tmp/tcell-wasm +cp webfiles/tcell.html webfiles/tcell.js webfiles/termstyle.css webfiles/beep.wav /tmp/tcell-wasm/ +cp -R webfiles/ghostty-web /tmp/tcell-wasm/ +cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" /tmp/tcell-wasm/ +GOOS=js GOARCH=wasm go build -o /tmp/tcell-wasm/main.wasm ./demos/unicode +python3 -m http.server -d /tmp/tcell-wasm 8080 +``` + In `tcell.js`, you also need to change the constant ```js const wasmFilePath = "yourfile.wasm" @@ -54,8 +73,24 @@ It is recommended to use an iframe if you want to embed the app into a webpage: ``` +### Sizing + +By default the web terminal fits itself to the size of the `#terminal` element and reacts to container resizes. The bundled `termstyle.css` makes this full-page by default. + +You can override the terminal cell dimensions explicitly in HTML: + +```html +

+```
+
+If only one of `data-cols` or `data-rows` is set, the other dimension remains reactive.
+
 ## Other considerations
 
 ### Accessing files
 
 `io.Open(filename)` and other related functions for reading file systems do not work; use `http.Get(filename)` instead.
+
+### Keyboard shortcuts
+
+The browser may reserve some key combinations before JavaScript can see or cancel them. This is especially common for Meta/Command shortcuts on macOS, such as Command-L. Standalone Meta key events can be reported, but Meta-modified key combinations are browser-dependent and should not be relied upon in WASM web mode.
diff --git a/vendor/github.com/gdamore/tcell/v3/README.md b/vendor/github.com/gdamore/tcell/v3/README.md
index f17609bb3..d37c163e7 100644
--- a/vendor/github.com/gdamore/tcell/v3/README.md
+++ b/vendor/github.com/gdamore/tcell/v3/README.md
@@ -119,6 +119,24 @@ than respecting themes. For other cases, such as typical text apps that
 only use a few colors, its more desirable to respect the themes that
 the user has established.)
 
+## Terminal Overrides
+
+_Tcell_ normally negotiates terminal capabilities automatically, but some
+terminal emulators answer those queries incorrectly. These environment
+variables provide user escape hatches when the automatic path is not reliable:
+
+- `TCELL_KEYBOARD_PROTOCOL=auto|legacy|kitty|win32|xterm` forces the keyboard
+  reporting protocol.
+- `TCELL_NEGOTIATE=auto|disable` disables startup capability negotiation when
+  terminal responses themselves are problematic.
+- `TCELL_MOUSE=auto|disable` prevents applications from enabling terminal mouse
+  reporting.
+
+Applications can also choose a keyboard protocol with `OptKeyboardProtocol` or
+disable startup negotiation with `OptNegotiation`. Environment variables take
+precedence so users can recover from bad terminal behavior without modifying an
+application.
+
 ## Performance
 
 Reasonable attempts have been made to minimize sending data to terminals,
diff --git a/vendor/github.com/gdamore/tcell/v3/cell.go b/vendor/github.com/gdamore/tcell/v3/cell.go
index eb2e92f76..cbe2732de 100644
--- a/vendor/github.com/gdamore/tcell/v3/cell.go
+++ b/vendor/github.com/gdamore/tcell/v3/cell.go
@@ -25,7 +25,15 @@ type cell struct {
 
 func (c *cell) setDirty(dirty bool) {
 	if dirty {
-		c.lastStr = ""
+		// Empty cells use currStr == "" until they are first drawn, at which
+		// point SetDirty(false) normalizes them to a space.  Using "" as the
+		// dirty marker for an untouched empty cell would therefore leave
+		// lastStr == currStr and fail to force a redraw.
+		if c.currStr == "" {
+			c.lastStr = " "
+		} else {
+			c.lastStr = ""
+		}
 	} else {
 		if c.currStr == "" {
 			c.currStr = " "
@@ -42,9 +50,10 @@ func (c *cell) setDirty(dirty bool) {
 //
 // CellBuffer is not thread safe.
 type CellBuffer struct {
-	w     int
-	h     int
-	cells []cell
+	w               int
+	h               int
+	cells           []cell
+	sanitizeContent bool
 }
 
 // Put a single styled grapheme using the given string and style
@@ -52,6 +61,13 @@ type CellBuffer struct {
 // will be displayed, using only the 1 or 2 (depending on width) cells
 // located at x, y. It returns the rest of the string, and the width used.
 func (cb *CellBuffer) Put(x int, y int, str string, style Style) (string, int) {
+	if cb.sanitizeContent {
+		str = stripOSCControlsIfNeeded(str)
+	}
+	return cb.put(x, y, str, style)
+}
+
+func (cb *CellBuffer) put(x int, y int, str string, style Style) (string, int) {
 	var width int = 0
 	if x >= 0 && y >= 0 && x < cb.w && y < cb.h {
 		var cl string
@@ -118,7 +134,7 @@ func (cb *CellBuffer) Size() (int, int) {
 // Invalidate marks all characters within the buffer as dirty.
 func (cb *CellBuffer) Invalidate() {
 	for i := range cb.cells {
-		cb.cells[i].lastStr = ""
+		cb.cells[i].setDirty(true)
 	}
 }
 
diff --git a/vendor/github.com/gdamore/tcell/v3/input.go b/vendor/github.com/gdamore/tcell/v3/input.go
index 32e0452e6..58c8a7e0d 100644
--- a/vendor/github.com/gdamore/tcell/v3/input.go
+++ b/vendor/github.com/gdamore/tcell/v3/input.go
@@ -22,8 +22,8 @@
 // There is unfortunately *one* conflict, with aixterm, for CSI-P - which is KeyDelete
 // in aixterm, but F1 in others.
 
-//go:build !js && !wasm
-// +build !js,!wasm
+//go:build (!js && !wasm) || (js && wasm)
+// +build !js,!wasm js,wasm
 
 package tcell
 
@@ -59,31 +59,61 @@ const (
 	istXda  // extended device attributes (ESC P Ps ST)
 )
 
+// defaultControlStringLimit caps inbound OSC/XDA control-string payloads
+// before they can grow without bound while waiting for a string terminator.
+const defaultControlStringLimit = 64 * 1024
+
 func newInputParser(eq chan<- Event) *inputParser {
 	return &inputParser{
-		evch: eq,
-		buf:  make([]rune, 0, 128),
+		evch:             eq,
+		buf:              make([]rune, 0, 128),
+		controlStringMax: defaultControlStringLimit,
 	}
 }
 
 type inputParser struct {
-	buf       []rune       // bytes to process (ingest data)
-	utfBuf    []byte       // accrued UTF8 bytes
-	strBuf    []byte       // accrued string data (for ST, OSC, etc.)
-	csiParams []byte       // accrued parameter bytes for CSI (and SS3)
-	csiInterm []byte       // accrued intermediate bytes for CSI
-	escChar   byte         // last byte for escape
-	escaped   bool         // true if next key should be modified by ESC
-	btnsDown  ButtonMask   // mouse buttons down (excludes wheel buttons)
-	state     inputState   // tracks processor state
-	strState  inputState   // saved str state (needed for ST)
-	l         sync.Mutex   // protects local state
-	evch      chan<- Event // where events are routed
-	rows      int          // used for clipping mouse coordinates
-	cols      int          // used for clipping mouse coordinates
-	keyTime   time.Time    // time of last key press / byte ingested
-	nested    *inputParser // for buggy win32-input-mode implementations
-	surrogate rune         // high surrogate pair seen (for Win32 input mode)
+	buf              []rune       // bytes to process (ingest data)
+	utfBuf           []byte       // accrued UTF8 bytes
+	strBuf           []byte       // accrued string data (for ST, OSC, etc.)
+	csiParams        []byte       // accrued parameter bytes for CSI (and SS3)
+	csiInterm        []byte       // accrued intermediate bytes for CSI
+	escChar          byte         // last byte for escape
+	escaped          bool         // true if next key should be modified by ESC
+	btnsDown         ButtonMask   // mouse buttons down (excludes wheel buttons)
+	state            inputState   // tracks processor state
+	strState         inputState   // saved str state (needed for ST)
+	l                sync.Mutex   // protects local state
+	evch             chan<- Event // where events are routed
+	rows             int          // used for clipping mouse coordinates
+	cols             int          // used for clipping mouse coordinates
+	pixelMouse       bool         // mouse reports in pixels (CSI ?1016h); skip cell clipping
+	keyTime          time.Time    // time of last key press / byte ingested
+	nested           *inputParser // for buggy win32-input-mode implementations
+	surrogate        rune         // high surrogate pair seen (for Win32 input mode)
+	advanced         bool         // use advanced key reporting semantics
+	controlStringMax int          // maximum inbound OSC/XDA payload size; 0 means unlimited
+	discardString    bool         // drop the rest of an over-limit OSC/XDA sequence
+}
+
+func keyFromInt(n int) (Key, bool) {
+	if n < 0 || n > 32767 {
+		return 0, false
+	}
+	return Key(n), true
+}
+
+func keyFromRune(r rune) (Key, bool) {
+	if r < 0 || r > 32767 {
+		return 0, false
+	}
+	return Key(r), true
+}
+
+func asciiByteFromInt(n int) (byte, bool) {
+	if n <= 0 || n >= 0x80 {
+		return 0, false
+	}
+	return byte(n), true
 }
 
 // Waiting returns true if the processor is waiting for
@@ -99,6 +129,21 @@ func (ip *inputParser) Waiting() bool {
 	return ip.state != istInit
 }
 
+// SetPixelMouse toggles whether SGR mouse reports are interpreted as
+// pixel coordinates (CSI ?1016h) rather than character cells (CSI ?1006h).
+// When enabled, mouse coordinates are not clipped to the screen size.
+// The setting is also forwarded to the lazily-created nested parser used
+// for win32-input-mode, if one exists, so both stay in sync.
+func (ip *inputParser) SetPixelMouse(on bool) {
+	ip.l.Lock()
+	ip.pixelMouse = on
+	nested := ip.nested
+	ip.l.Unlock()
+	if nested != nil {
+		nested.SetPixelMouse(on)
+	}
+}
+
 func (ip *inputParser) SetSize(w, h int) {
 	if ip.nested != nil {
 		ip.nested.SetSize(w, h)
@@ -116,7 +161,7 @@ func (ip *inputParser) post(ev Event) {
 	if ip.escaped {
 		ip.escaped = false
 		if ke, ok := ev.(*EventKey); ok {
-			ev = NewEventKey(ke.Key(), ke.Str(), ke.Modifiers()|ModAlt)
+			ev = ip.newKey(ke.Key(), ke.Str(), ke.Modifiers()|ModAlt, ke.Pressed(), ke.Physical(), ke.Repeat())
 		}
 	} else if ke, ok := ev.(*EventKey); ok {
 		switch ke.Key() {
@@ -130,6 +175,31 @@ func (ip *inputParser) post(ev Event) {
 	ip.evch <- ev
 }
 
+func (ip *inputParser) newKey(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int) *EventKey {
+	if ip.advanced {
+		return NewEventKeyEx(k, str, mod, pressed, physical, repeat)
+	}
+	return NewEventKey(k, str, mod)
+}
+
+func (ip *inputParser) postKey(k Key, str string, mod ModMask) {
+	ip.post(ip.newKey(k, str, mod, true, 0, 1))
+}
+
+func (ip *inputParser) postKeyEx(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int) {
+	ip.post(ip.newKey(k, str, mod, pressed, physical, repeat))
+}
+
+func (ip *inputParser) postControlKey(r rune, mod ModMask) {
+	if r == 0 {
+		ip.postKeyEx(KeyRune, " ", mod|ModCtrl, true, Key(' '), 1)
+	} else if ip.advanced && r >= 1 && r <= 26 {
+		ip.postKeyEx(KeyRune, string('a'+r-1), mod|ModCtrl, true, Key('a'+r-1), 1)
+	} else {
+		ip.postKey(KeyRune, string(r+0x40), mod|ModCtrl)
+	}
+}
+
 type csiParamMode struct {
 	M rune // Mode
 	P int  // Parameter (first)
@@ -358,6 +428,14 @@ var csiUKeys = map[int]keyMap{
 	57425: {Key: KeyInsert},          // KP_INSERT
 	57426: {Key: KeyDelete},          // KP_DELETE
 	// 57427: {Key: KeyBegin},          // KP_BEGIN
+	57441: {Key: KeyShift}, // LEFT_SHIFT
+	57442: {Key: KeyCtrl},  // LEFT_CONTROL
+	57443: {Key: KeyAlt},   // LEFT_ALT
+	57444: {Key: KeyMeta},  // LEFT_SUPER
+	57447: {Key: KeyShift}, // RIGHT_SHIFT
+	57448: {Key: KeyCtrl},  // RIGHT_CONTROL
+	57449: {Key: KeyAlt},   // RIGHT_ALT
+	57450: {Key: KeyMeta},  // RIGHT_SUPER
 
 	// TODO: Media keys
 }
@@ -447,7 +525,8 @@ func (ip *inputParser) scan() {
 		if r >= 0xA0 {
 			// 8-bit extended Unicode we just treat as such - this will swallow anything else queued up
 			ip.state = istInit
-			ip.post(NewEventKey(KeyRune, string(r), ModNone))
+			physical, _ := keyFromRune(r)
+			ip.postKeyEx(KeyRune, string(r), ModNone, true, physical, 1)
 			continue
 		} else if r >= 0x80 {
 			// ISO 2022 control chars
@@ -463,19 +542,20 @@ func (ip *inputParser) scan() {
 				ip.state = istEsc
 				ip.escChar = 0
 			case '\t':
-				ip.post(NewEventKey(KeyTab, "", ModNone))
+				ip.postKey(KeyTab, "", ModNone)
 			case '\b', '\x7F':
-				ip.post(NewEventKey(KeyBackspace, "", ModNone))
+				ip.postKey(KeyBackspace, "", ModNone)
 			case '\r':
-				ip.post(NewEventKey(KeyEnter, "", ModNone))
+				ip.postKey(KeyEnter, "", ModNone)
 			default:
 				// Control keys - legacy handling
 				if r == 0 {
-					ip.post(NewEventKey(KeyRune, " ", ModCtrl))
+					ip.postControlKey(r, ModNone)
 				} else if r < ' ' {
-					ip.post(NewEventKey(KeyRune, string(r+0x40), ModCtrl))
+					ip.postControlKey(r, ModNone)
 				} else {
-					ip.post(NewEventKey(KeyRune, string(r), ModNone))
+					physical, _ := keyFromRune(r)
+					ip.postKeyEx(KeyRune, string(r), ModNone, true, physical, 1)
 				}
 			}
 		case istEsc:
@@ -488,6 +568,7 @@ func (ip *inputParser) scan() {
 			case ']':
 				ip.state = istOsc
 				ip.strBuf = nil
+				ip.discardString = false
 				ip.escChar = byte(r)
 			case 'N':
 				ip.state = istSs2 // no known uses
@@ -502,6 +583,7 @@ func (ip *inputParser) scan() {
 				ip.state = istXda
 				ip.csiParams = nil
 				ip.strBuf = nil
+				ip.discardString = false
 				ip.escChar = byte(r)
 			case 'X':
 				ip.state = istSos
@@ -521,7 +603,7 @@ func (ip *inputParser) scan() {
 			case '\t':
 				// Linux console only, does not conform to ECMA
 				ip.state = istInit
-				ip.post(NewEventKey(KeyBacktab, "", ModNone))
+				ip.postKey(KeyBacktab, "", ModNone)
 			default:
 				if r == '\x1b' {
 					// leading ESC to capture alt
@@ -535,7 +617,8 @@ func (ip *inputParser) scan() {
 						mod |= ModCtrl
 						r += 0x60
 					}
-					ip.post(NewEventKey(KeyRune, string(r), mod))
+					physical, _ := keyFromRune(r)
+					ip.postKeyEx(KeyRune, string(r), mod, true, physical, 1)
 				}
 			}
 		case istCsi:
@@ -585,16 +668,16 @@ func (ip *inputParser) scan() {
 				// parameters that do not match one of these forms, we just discard it.
 				if len(ip.csiParams) == 0 {
 					// simple SS3 case
-					ip.post(NewEventKey(k, "", ModNone))
+					ip.postKey(k, "", ModNone)
 				} else if parts := strings.Split(string(ip.csiParams), ";"); len(parts) >= 1 {
 					// SS3 with modifier (old style).  Note old terminfo would declare these as high
 					// numbered function keys, but we encode as modified since that's how they are entered.
 					if len(parts) >= 2 {
 						if m, err := strconv.Atoi(parts[1]); err == nil && (parts[0] == "1" || parts[0] == "") {
-							ip.post(NewEventKey(k, "", calcModifier(m)))
+							ip.postKey(k, "", calcModifier(m))
 						}
 					} else if m, err := strconv.Atoi(parts[0]); err == nil {
-						ip.post(NewEventKey(k, "", calcModifier(m)))
+						ip.postKey(k, "", calcModifier(m))
 					}
 				}
 			}
@@ -614,9 +697,16 @@ func (ip *inputParser) scan() {
 				ip.strState = ip.state
 				ip.state = istSt
 			case '\x07':
-				ip.handleXda(string(ip.strBuf))
+				if ip.discardString {
+					ip.discardString = false
+					ip.state = istInit
+				} else {
+					ip.handleXda(string(ip.strBuf))
+				}
 			default:
-				ip.strBuf = append(ip.strBuf, byte(r&0x7f))
+				if !ip.discardString {
+					ip.appendStringBytes(byte(r & 0x7f))
+				}
 			}
 
 		case istOsc: // not sure if used
@@ -625,29 +715,42 @@ func (ip *inputParser) scan() {
 				ip.strState = ip.state
 				ip.state = istSt
 			case '\x07':
-				ip.handleOsc(string(ip.strBuf))
+				if ip.discardString {
+					ip.discardString = false
+					ip.state = istInit
+				} else {
+					ip.handleOsc(string(ip.strBuf))
+				}
 			default:
-				ip.strBuf = append(ip.strBuf, byte(r&0x7f))
+				if !ip.discardString {
+					ip.appendStringBytes(byte(r & 0x7f))
+				}
 			}
 		case istSt:
 			if r == '\\' || r == '\x07' {
 				ip.state = istInit
-				switch ip.strState {
-				case istOsc:
-					ip.handleOsc(string(ip.strBuf))
-				case istXda:
-					ip.handleXda(string(ip.strBuf))
-				case istPm, istApc, istSos, istDcs:
-					ip.state = istInit
+				if ip.discardString {
+					ip.discardString = false
+				} else {
+					switch ip.strState {
+					case istOsc:
+						ip.handleOsc(string(ip.strBuf))
+					case istXda:
+						ip.handleXda(string(ip.strBuf))
+					case istPm, istApc, istSos, istDcs:
+						ip.state = istInit
+					}
 				}
 			} else {
-				ip.strBuf = append(ip.strBuf, '\x1b', byte(r))
+				if !ip.discardString {
+					ip.appendStringBytes('\x1b', byte(r))
+				}
 				ip.state = ip.strState
 			}
 		case istLnx:
 			// linux console does not follow ECMA
 			if k, ok := linuxFKeys[r]; ok {
-				ip.post(NewEventKey(k, "", ModNone))
+				ip.postKey(k, "", ModNone)
 			}
 			ip.state = istInit
 		}
@@ -655,15 +758,25 @@ func (ip *inputParser) scan() {
 
 	if ip.state != istInit && time.Since(ip.keyTime) > time.Millisecond*50 {
 		if ip.state == istEsc {
-			ip.post(NewEventKey(KeyEscape, "", ModNone))
+			ip.postKey(KeyEscape, "", ModNone)
 		} else if ec := ip.escChar; ec != 0 {
-			ip.post(NewEventKey(KeyRune, string(ec), ModAlt))
+			ip.postKey(KeyRune, string(ec), ModAlt)
 		}
 		// if we take too long between bytes, reset the state machine.
 		ip.state = istInit
+		ip.discardString = false
 	}
 }
 
+func (ip *inputParser) appendStringBytes(bs ...byte) {
+	if ip.controlStringMax > 0 && len(ip.strBuf)+len(bs) > ip.controlStringMax {
+		ip.strBuf = nil
+		ip.discardString = true
+		return
+	}
+	ip.strBuf = append(ip.strBuf, bs...)
+}
+
 func (ip *inputParser) handleOsc(str string) {
 	ip.state = istInit
 	if content, ok := strings.CutPrefix(str, "52;c;"); ok {
@@ -717,6 +830,99 @@ func calcModifier(n int) ModMask {
 	return m
 }
 
+func calcWinModifier(n int, advanced bool) ModMask {
+	m := ModNone
+	if n&0x010 != 0 {
+		m |= ModShift
+	}
+	if advanced {
+		// Bits through 0x0100 match Win32 dwControlKeyState. 0x0040 and
+		// 0x0080 are ScrollLock and CapsLock, not Meta. The 0x0200 and
+		// 0x0400 bits are tcell extensions used by the WASM browser shim,
+		// which has Meta keys but no native Win32 bit assignment for them.
+		if n&0x0008 != 0 {
+			m |= ModLCtrl
+		}
+		if n&0x0004 != 0 {
+			m |= ModRCtrl
+		}
+		if n&0x0002 != 0 {
+			m |= ModLAlt
+		}
+		if n&0x0001 != 0 {
+			m |= ModRAlt
+		}
+		if n&0x0200 != 0 {
+			m |= ModLMeta
+		}
+		if n&0x0400 != 0 {
+			m |= ModRMeta
+		}
+	} else {
+		if n&0x000c != 0 {
+			m |= ModCtrl
+		}
+		if n&0x0003 != 0 {
+			m |= ModAlt
+		}
+	}
+	return m
+}
+
+func winModifierKey(vk int) (Key, ModMask, bool) {
+	switch vk {
+	case 0x10:
+		return KeyShift, ModShift, true
+	case 0xa0:
+		return KeyShift, ModLShift, true
+	case 0xa1:
+		return KeyShift, ModRShift, true
+	case 0x11:
+		return KeyCtrl, ModCtrl, true
+	case 0xa2:
+		return KeyCtrl, ModLCtrl, true
+	case 0xa3:
+		return KeyCtrl, ModRCtrl, true
+	case 0x12:
+		return KeyAlt, ModAlt, true
+	case 0xa4:
+		return KeyAlt, ModLAlt, true
+	case 0xa5:
+		return KeyAlt, ModRAlt, true
+	case 0x5b:
+		return KeyMeta, ModLMeta, true
+	case 0x5c:
+		return KeyMeta, ModRMeta, true
+	case 0x14:
+		return KeyCapsLock, ModNone, true
+	default:
+		return 0, ModNone, false
+	}
+}
+
+func kittyModifierKey(code int) ModMask {
+	switch code {
+	case 57441:
+		return ModLShift
+	case 57447:
+		return ModRShift
+	case 57442:
+		return ModLCtrl
+	case 57448:
+		return ModRCtrl
+	case 57443:
+		return ModLAlt
+	case 57449:
+		return ModRAlt
+	case 57444:
+		return ModLMeta
+	case 57450:
+		return ModRMeta
+	default:
+		return ModNone
+	}
+}
+
 func (ip *inputParser) handleMouse(mode rune, params []int) {
 
 	// XTerm mouse events only report at most one button at a time,
@@ -729,9 +935,15 @@ func (ip *inputParser) handleMouse(mode rune, params []int) {
 	btn := params[0]
 	// Some terminals will report mouse coordinates outside the
 	// screen, especially with click-drag events.  Clip the coordinates
-	// to the screen in that case.
-	x := max(min(params[1]-1, ip.cols-1), 0)
-	y := max(min(params[2]-1, ip.rows-1), 0)
+	// to the screen in that case.  In pixel-reporting mode (CSI ?1016h)
+	// the values are already pixels rather than cells, so skip the clip
+	// and pass them through unchanged for the application to interpret.
+	x := params[1] - 1
+	y := params[2] - 1
+	if !ip.pixelMouse {
+		x = max(min(x, ip.cols-1), 0)
+		y = max(min(y, ip.rows-1), 0)
+	}
 
 	button := ButtonNone
 	mod := ModNone
@@ -827,7 +1039,7 @@ func (ip *inputParser) handleWinKey(P []int) {
 	for len(P) < 6 {
 		P = append(P, 0) // ensure sufficient length
 	}
-	if P[3] == 0 {
+	if P[3] == 0 && !ip.advanced {
 		// key up event ignore ignore
 		return
 	}
@@ -835,58 +1047,81 @@ func (ip *inputParser) handleWinKey(P []int) {
 	// these terminals never send ambiguous escapes
 	ip.escaped = false
 
-	if P[0] == 0 && P[1] == 0 && P[2] > 0 && P[2] < 0x80 { // only ASCII in win32-input-mode
-		if ip.nested == nil {
-			ip.nested = &inputParser{
-				evch: ip.evch,
-				rows: ip.rows,
-				cols: ip.cols,
+	if P[0] == 0 && P[1] == 0 { // only ASCII in win32-input-mode
+		if b, ok := asciiByteFromInt(P[2]); ok {
+			if ip.nested == nil {
+				ip.nested = &inputParser{
+					evch:             ip.evch,
+					rows:             ip.rows,
+					cols:             ip.cols,
+					advanced:         ip.advanced,
+					pixelMouse:       ip.pixelMouse,
+					controlStringMax: ip.controlStringMax,
+				}
 			}
+			ip.nested.ScanUTF8([]byte{b})
+			return
 		}
-		if P[2] > 0 {
-			ip.nested.ScanUTF8([]byte{byte(P[2])})
-		}
-		return
 	}
 
 	key := KeyRune
 	chr := rune(P[2])
 	mod := ModNone
 	rpt := max(1, P[5])
+	decoded := false
 	if k1, ok := winKeys[P[0]]; ok {
 		chr = 0
 		key = k1
+		decoded = true
+	} else if ip.advanced {
+		if k1, mod1, ok := winModifierKey(P[0]); ok {
+			key = k1
+			mod = mod1
+			chr = 0
+			decoded = true
+		}
+	}
+	if decoded {
+		// Already decoded.
 	} else if chr == 0 && P[0] >= 0x30 && P[0] <= 0x39 {
 		chr = rune(P[0])
 	} else if chr < ' ' && P[0] >= 0x41 && P[0] <= 0x5a {
-		key = Key(P[0])
-		chr = 0
+		if ip.advanced {
+			key = KeyRune
+			chr = rune(P[0] + 0x20)
+		} else {
+			var ok bool
+			if key, ok = keyFromInt(P[0]); !ok {
+				return
+			}
+			chr = 0
+		}
 	} else if chr >= 0xD800 && chr <= 0xDBFF {
 		// high surrogate pair
+		if ip.surrogate != 0 {
+			ip.postKeyEx(KeyRune, string(utf8.RuneError), mod, P[3] != 0, 0, rpt)
+		}
 		ip.surrogate = chr
 		return
 	} else if chr >= 0xDC00 && chr <= 0xDFFF {
 		// low surrogate pair
-		chr = utf16.DecodeRune(ip.surrogate, chr)
-	} else if P[0] == 0x10 || P[0] == 0x11 || P[0] == 0x12 || P[0] == 0x14 {
-		// lone modifiers
+		if ip.surrogate == 0 {
+			chr = utf8.RuneError
+		} else {
+			chr = utf16.DecodeRune(ip.surrogate, chr)
+		}
+	} else if ip.surrogate != 0 {
+		ip.postKeyEx(KeyRune, string(utf8.RuneError), mod, P[3] != 0, 0, rpt)
+	} else if _, _, ok := winModifierKey(P[0]); ok {
+		// Lone modifier releases are ignored unless advanced mode is enabled.
 		ip.surrogate = 0
 		return
 	}
 
 	ip.surrogate = 0
 
-	// Modifiers
-	if P[4]&0x010 != 0 {
-		mod |= ModShift
-	}
-	if P[4]&0x000c != 0 {
-		mod |= ModCtrl
-	}
-	if P[4]&0x0003 != 0 {
-		mod |= ModAlt
-	}
-	if key == KeyRune && chr > ' ' && mod == ModShift {
+	mod |= calcWinModifier(P[4], ip.advanced)
+	if key == KeyRune && chr > ' ' && mod == ModShift && !ip.advanced {
 		// filter out lone shift for printable chars
 		mod = ModNone
 	}
@@ -895,13 +1130,18 @@ func (ip *inputParser) handleWinKey(P []int) {
 		mod = ModNone
 	}
 
-	for range rpt {
-		if key != KeyRune {
-			ip.post(NewEventKey(key, "", mod))
-		} else if chr != 0 {
-			ip.post(NewEventKey(KeyRune, string(chr), mod))
+	physical := key
+	if key == KeyRune && chr != 0 {
+		physical, _ = keyFromRune(chr)
+		if ip.advanced && P[0] >= 0x41 && P[0] <= 0x5a {
+			physical, _ = keyFromInt(P[0] + 0x20)
 		}
 	}
+	if key != KeyRune {
+		ip.postKeyEx(key, "", mod, P[3] != 0, physical, rpt)
+	} else if chr != 0 {
+		ip.postKeyEx(KeyRune, string(chr), mod, P[3] != 0, physical, rpt)
+	}
 }
 
 func (ip *inputParser) handlePrimaryDA(params []int) {
@@ -976,7 +1216,6 @@ func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte)
 	// reset state
 	ip.state = istInit
 
-	var parts []string
 	var P []int
 	hasLT := false
 	hasQM := false
@@ -994,16 +1233,54 @@ func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte)
 		pstr = pstr[1:]
 	}
 
+	pressed := true
+	repeat := 1
+	physical := Key(0)
 	if pstr != "" && pstr[0] >= '0' && pstr[0] <= '9' {
-		parts = strings.Split(pstr, ";")
+		var PSubs [][]int
+
+		parts := strings.Split(pstr, ";")
 		for i := range parts {
-			if parts[i] != "" {
-				if n, e := strconv.ParseInt(parts[i], 10, 32); e == nil {
+			subparts := strings.Split(parts[i], ":")
+			if subparts[0] != "" {
+				if n, e := strconv.ParseInt(subparts[0], 10, 32); e == nil {
 					P = append(P, int(n))
+				} else {
+					P = append(P, 0)
 				}
 			} else {
 				P = append(P, 0)
 			}
+			subs := []int{}
+			for _, sub := range subparts[1:] {
+				if sub != "" {
+					if n, e := strconv.ParseInt(sub, 10, 32); e == nil {
+						subs = append(subs, int(n))
+					}
+				} else {
+					subs = append(subs, 0)
+				}
+			}
+			PSubs = append(PSubs, subs)
+		}
+		if len(PSubs) > 1 && len(PSubs[1]) > 0 {
+			switch PSubs[1][0] {
+			case 2:
+				repeat = 2
+			case 3:
+				pressed = false
+			}
+		}
+		if len(PSubs) > 0 && len(PSubs[0]) > 0 {
+			base := PSubs[0][0]
+			if baseKey, ok := csiUKeys[base]; ok {
+				physical = baseKey.Key
+				if physical == KeyRune && baseKey.Rune != 0 {
+					physical, _ = keyFromRune(baseKey.Rune)
+				}
+			} else if base != 0 {
+				physical, _ = keyFromInt(base)
+			}
 		}
 	}
 	var P0 int
@@ -1076,10 +1353,13 @@ func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte)
 			if len(P) > 1 {
 				mod = calcModifier(P[1])
 			}
+			if mod1 := kittyModifierKey(P0); mod1 != ModNone {
+				mod |= mod1
+			}
 			if key != KeyRune {
-				ip.post(NewEventKey(key, "", mod))
+				ip.postKeyEx(key, "", mod, pressed, physical, repeat)
 			} else if chr != 0 {
-				ip.post(NewEventKey(KeyRune, string(chr), mod))
+				ip.postKeyEx(KeyRune, string(chr), mod, pressed, physical, repeat)
 			}
 			return
 		}
@@ -1114,14 +1394,17 @@ func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte)
 		if len(P) >= 2 {
 			mod := calcModifier(P[1])
 			if ks, ok := csiAllKeys[csiParamMode{M: mode, P: P0}]; ok {
-				ip.post(NewEventKey(ks.Key, "", mod))
+				ip.postKeyEx(ks.Key, "", mod, pressed, 0, repeat)
 				return
 			}
-			if P0 == 27 && len(P) > 2 && P[2] > 0 && P[2] <= 0xff {
+			if P0 == 27 && len(P) > 2 && P[2] > 0 && P[2] <= utf8.MaxRune {
 				if P[2] < ' ' || P[2] == 0x7F {
-					ip.post(NewEventKey(Key(P[2]), "", mod))
+					if key, ok := keyFromInt(P[2]); ok {
+						ip.postKey(key, "", mod)
+					}
 				} else {
-					ip.post(NewEventKey(KeyRune, string(rune(P[2])), mod))
+					physical, _ := keyFromRune(rune(P[2]))
+					ip.postKeyEx(KeyRune, string(rune(P[2])), mod, true, physical, 1)
 				}
 				return
 			}
@@ -1135,13 +1418,13 @@ func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte)
 		} else if mode == 'P' && os.Getenv("TERM") == "aixterm" {
 			ks.Key = KeyDelete // aixterm hack - conflicts with kitty protocol
 		}
-		ip.post(NewEventKey(ks.Key, "", ks.Mod))
+		ip.postKey(ks.Key, "", ks.Mod)
 		return
 	}
 
 	// this might have been an SS3 style key with modifiers applied
 	if k, ok := ss3Keys[mode]; ok && P0 == 1 && len(P) > 1 {
-		ip.post(NewEventKey(k, "", calcModifier(P[1])))
+		ip.postKeyEx(k, "", calcModifier(P[1]), pressed, 0, repeat)
 		return
 	}
 	// if we got here we just swallow the unknown sequence
diff --git a/vendor/github.com/gdamore/tcell/v3/key.go b/vendor/github.com/gdamore/tcell/v3/key.go
index db4d79b21..bb4c790ea 100644
--- a/vendor/github.com/gdamore/tcell/v3/key.go
+++ b/vendor/github.com/gdamore/tcell/v3/key.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The TCell Authors
+// Copyright 2026 The TCell Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -45,9 +45,12 @@ import (
 // specific keys.
 type EventKey struct {
 	EventTime
-	mod ModMask
-	key Key
-	str string // string for key, usually just one character, but may be composed sequence
+	mod      ModMask
+	key      Key
+	physical Key
+	str      string // string for key, usually just one character, but may be composed sequence
+	pressed  bool
+	repeat   int
 }
 
 // Str returns the string corresponding to the key press, if it makes sense.
@@ -66,6 +69,38 @@ func (ev *EventKey) Key() Key {
 	return ev.key
 }
 
+// Physical returns the physical key that was pressed, when known.
+//
+// This is different from Key() and Str(), which describe the logical key result
+// delivered to the application.  For example, on a US keyboard Shift-/ may
+// produce Str() == "?", while Physical() reports KeySlash.  Most applications
+// should use Key() and Str(); Physical is intended for layout-independent uses
+// such as keyboard remappers, embedded terminal emulators, and games that care
+// about key location rather than the printed character.
+//
+// For letter keys, compare physical values against the lowercase aliases
+// KeyA through KeyZ.  The legacy KeyCtrlA through KeyCtrlZ constants occupy
+// the same numeric range as Key('A') through Key('Z'), so Key('A') is not a
+// physical "A" key identifier.
+//
+// If the physical key is unknown, this returns zero.
+func (ev *EventKey) Physical() Key {
+	return ev.physical
+}
+
+// Pressed returns true for key press events, and false for key release events.
+// Legacy keyboard reporting only reports presses.
+func (ev *EventKey) Pressed() bool {
+	return ev.pressed
+}
+
+// Repeat returns the repeat count for this key event.  Legacy keyboard
+// reporting synthesizes repeated key presses as separate events, so this will
+// normally be 1.
+func (ev *EventKey) Repeat() int {
+	return ev.repeat
+}
+
 // Modifiers returns the modifiers that were present with the key press.  Note
 // that not all platforms and terminals support this equally well, and some
 // cases we will not not know for sure.  Hence, applications should avoid
@@ -74,6 +109,19 @@ func (ev *EventKey) Modifiers() ModMask {
 	return ev.mod
 }
 
+// KeyProtocol identifies the keyboard reporting protocol that the terminal
+// is currently using.  More capable protocols allow disambiguating modifier
+// combinations, distinguishing key release events, etc.
+type KeyProtocol int
+
+// These are the keyboard protocols that tcell can report.
+const (
+	LegacyKeyboard KeyProtocol = iota // basic VT100 style reports
+	KittyKeyboard                     // kitty supports events, unambiguous keys modulo left/right modifiers
+	Win32Keyboard                     // win32 supports the full feature set
+	XTermKeyboard                     // xterm modify other keys, disambiguation only, no release events
+)
+
 // KeyNames holds the written names of special keys. Useful to echo back a key
 // name, or to look up a key from a string value.
 var KeyNames = map[Key]string{
@@ -171,6 +219,11 @@ var KeyNames = map[Key]string{
 	KeyCapsLock:   "CapsLock",
 	KeyScrollLock: "ScrollLock",
 	KeyNumLock:    "NumLock",
+	KeyShift:      "Shift",
+	KeyCtrl:       "Ctrl",
+	KeyAlt:        "Alt",
+	KeyMeta:       "Meta",
+	KeyHyper:      "Hyper",
 	KeyCtrlA:      "Ctrl-A",
 	KeyCtrlB:      "Ctrl-B",
 	KeyCtrlC:      "Ctrl-C",
@@ -204,19 +257,49 @@ var KeyNames = map[Key]string{
 func (ev *EventKey) Name() string {
 	s := ""
 	m := []string{}
-	if ev.mod&ModShift != 0 {
+	if ev.mod&modLShift != 0 {
+		m = append(m, "LeftShift")
+	}
+	if ev.mod&modRShift != 0 {
+		m = append(m, "RightShift")
+	}
+	if ev.mod&ModShift != 0 && ev.mod&(modLShift|modRShift) == 0 {
 		m = append(m, "Shift")
 	}
-	if ev.mod&ModAlt != 0 {
+	if ev.mod&modLAlt != 0 {
+		m = append(m, "LeftAlt")
+	}
+	if ev.mod&modRAlt != 0 {
+		m = append(m, "RightAlt")
+	}
+	if ev.mod&ModAlt != 0 && ev.mod&(modLAlt|modRAlt) == 0 {
 		m = append(m, "Alt")
 	}
-	if ev.mod&ModMeta != 0 {
+	if ev.mod&modLMeta != 0 {
+		m = append(m, "LeftMeta")
+	}
+	if ev.mod&modRMeta != 0 {
+		m = append(m, "RightMeta")
+	}
+	if ev.mod&ModMeta != 0 && ev.mod&(modLMeta|modRMeta) == 0 {
 		m = append(m, "Meta")
 	}
-	if ev.mod&ModCtrl != 0 {
+	if ev.mod&modLCtrl != 0 {
+		m = append(m, "LeftCtrl")
+	}
+	if ev.mod&modRCtrl != 0 {
+		m = append(m, "RightCtrl")
+	}
+	if ev.mod&ModCtrl != 0 && ev.mod&(modLCtrl|modRCtrl) == 0 {
 		m = append(m, "Ctrl")
 	}
-	if ev.mod&ModHyper != 0 {
+	if ev.mod&modLHyper != 0 {
+		m = append(m, "LeftHyper")
+	}
+	if ev.mod&modRHyper != 0 {
+		m = append(m, "RightHyper")
+	}
+	if ev.mod&ModHyper != 0 && ev.mod&(modLHyper|modRHyper) == 0 {
 		m = append(m, "Hyper")
 	}
 
@@ -229,6 +312,28 @@ func (ev *EventKey) Name() string {
 		}
 	}
 	if len(m) != 0 {
+		switch ev.key {
+		case KeyShift:
+			if ev.mod&(modLShift|modRShift|ModShift) != 0 {
+				return strings.Join(m, "+")
+			}
+		case KeyCtrl:
+			if ev.mod&(modLCtrl|modRCtrl|ModCtrl) != 0 {
+				return strings.Join(m, "+")
+			}
+		case KeyAlt:
+			if ev.mod&(modLAlt|modRAlt|ModAlt) != 0 {
+				return strings.Join(m, "+")
+			}
+		case KeyMeta:
+			if ev.mod&(modLMeta|modRMeta|ModMeta) != 0 {
+				return strings.Join(m, "+")
+			}
+		case KeyHyper:
+			if ev.mod&(modLHyper|modRHyper|ModHyper) != 0 {
+				return strings.Join(m, "+")
+			}
+		}
 		if ev.mod&ModCtrl != 0 && strings.HasPrefix(s, "Ctrl-") {
 			s = s[5:]
 		}
@@ -242,10 +347,25 @@ func (ev *EventKey) Name() string {
 // has more precise information it should set that specifically.  Callers
 // that aren't sure about modifier state (most) should just pass ModNone.
 func NewEventKey(k Key, str string, mod ModMask) *EventKey {
+	return newEventKey(k, str, mod, true, 0, 1, false)
+}
+
+// NewEventKeyEx creates an extended key event with press/release, physical key,
+// and repeat metadata.  It also uses the newer key normalization rules: ASCII
+// control letters are reported as KeyRune plus ModCtrl instead of legacy
+// KeyCtrlA through KeyCtrlZ values.
+func NewEventKeyEx(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int) *EventKey {
+	return newEventKey(k, str, mod, pressed, physical, repeat, true)
+}
+
+func newEventKey(k Key, str string, mod ModMask, pressed bool, physical Key, repeat int, advanced bool) *EventKey {
 	ch := rune(0)
 	if len(str) == 1 {
 		ch = []rune(str)[0]
 	}
+	if repeat <= 0 {
+		repeat = 1
+	}
 
 	if k == KeyRune {
 		if ch != 0 && (ch < ' ' || ch == 0x7f) {
@@ -267,7 +387,7 @@ func NewEventKey(k Key, str string, mod ModMask) *EventKey {
 
 		// For legacy reasons, if Ctrl is pressed with an ASCII alphabetic, then we
 		// emit it as a KeyCtrlXX symbol.
-		if mod == ModCtrl {
+		if mod == ModCtrl && !advanced {
 			// We don't do Ctrl-[ or backslash or those specially.
 			if ch >= 'A' && ch <= 'Z' { // upper case
 				k = KeyCtrlA + Key(ch-'A')
@@ -280,7 +400,7 @@ func NewEventKey(k Key, str string, mod ModMask) *EventKey {
 
 		// Windows reports ModShift for shifted keys.  This is inconsistent
 		// with UNIX, lets harmonize this.
-		if mod == ModShift && str != "" {
+		if mod == ModShift && str != "" && !advanced {
 			mod = ModNone
 		}
 	}
@@ -290,19 +410,29 @@ func NewEventKey(k Key, str string, mod ModMask) *EventKey {
 		k = KeyBackspace
 	}
 
+	// Advanced key reporting exposes Shift-Tab directly.  Backtab is a legacy
+	// alias from terminals that cannot distinguish a physical Backtab key.
+	if k == KeyBacktab && advanced {
+		k = KeyTab
+		mod |= ModShift
+		if physical == 0 || physical == KeyBacktab {
+			physical = KeyTab
+		}
+	}
+
 	// Shift-Tab should be Backtab.
-	if k == KeyTab && (mod&ModShift) != 0 {
+	if k == KeyTab && (mod&ModShift) != 0 && !advanced {
 		k = KeyBacktab
 		mod &^= ModShift
 	}
-	ev := &EventKey{key: k, str: str, mod: mod}
+	ev := &EventKey{key: k, str: str, mod: mod, pressed: pressed, physical: physical, repeat: repeat}
 	ev.SetEventNow()
 	return ev
 }
 
 // ModMask is a mask of modifier keys.  Note that it will not always be
 // possible to report modifier keys.
-type ModMask int16
+type ModMask int32
 
 // These are the modifiers keys that can be sent either with a key press,
 // or a mouse event.  Note that as of now, due to the confusion associated
@@ -318,6 +448,103 @@ const (
 	ModNone ModMask = 0
 )
 
+const (
+	modLShift ModMask = 1 << (iota + 5)
+	modRShift
+	modLCtrl
+	modRCtrl
+	modLAlt
+	modRAlt
+	modLMeta
+	modRMeta
+	modLHyper
+	modRHyper
+)
+
+// These modifiers identify a specific side when the keyboard protocol reports
+// one.  They include the aggregate modifier bit, so ModLCtrl also satisfies
+// checks for ModCtrl.
+const (
+	ModLShift = ModShift | modLShift
+	ModRShift = ModShift | modRShift
+	ModLCtrl  = ModCtrl | modLCtrl
+	ModRCtrl  = ModCtrl | modRCtrl
+	ModLAlt   = ModAlt | modLAlt
+	ModRAlt   = ModAlt | modRAlt
+	ModLMeta  = ModMeta | modLMeta
+	ModRMeta  = ModMeta | modRMeta
+	ModLHyper = ModHyper | modLHyper
+	ModRHyper = ModHyper | modRHyper
+)
+
+// These keys are aliases for printable physical keys.  They are primarily
+// useful with EventKey.Physical, which may report a base key location separately
+// from the generated text.
+//
+// These names identify the unshifted base key on a US-style keyboard layout.
+// They do not identify logical characters produced by modifiers or other
+// layouts.  For example, the physical key named KeySlash may produce "/" or
+// "?" on a US keyboard depending on Shift, and may produce different text on
+// other layouts.  Applications interested in the logical key sequence should
+// use EventKey.Key and EventKey.Str instead.
+const (
+	KeySpace Key = ' '
+	Key0     Key = '0'
+	Key1     Key = '1'
+	Key2     Key = '2'
+	Key3     Key = '3'
+	Key4     Key = '4'
+	Key5     Key = '5'
+	Key6     Key = '6'
+	Key7     Key = '7'
+	Key8     Key = '8'
+	Key9     Key = '9'
+
+	KeyGrave      Key = '`'
+	KeyBacktick   Key = KeyGrave
+	KeyMinus      Key = '-'
+	KeyEqual      Key = '='
+	KeyLBrace     Key = '['
+	KeyLBracket   Key = KeyLBrace
+	KeyRBrace     Key = ']'
+	KeyRBracket   Key = KeyRBrace
+	KeyBackslash  Key = '\\'
+	KeySemi       Key = ';'
+	KeySemicolon  Key = KeySemi
+	KeyQuote      Key = '\''
+	KeyApostrophe Key = KeyQuote
+	KeyComma      Key = ','
+	KeyPeriod     Key = '.'
+	KeySlash      Key = '/'
+
+	KeyA Key = 'a'
+	KeyB Key = 'b'
+	KeyC Key = 'c'
+	KeyD Key = 'd'
+	KeyE Key = 'e'
+	KeyF Key = 'f'
+	KeyG Key = 'g'
+	KeyH Key = 'h'
+	KeyI Key = 'i'
+	KeyJ Key = 'j'
+	KeyK Key = 'k'
+	KeyL Key = 'l'
+	KeyM Key = 'm'
+	KeyN Key = 'n'
+	KeyO Key = 'o'
+	KeyP Key = 'p'
+	KeyQ Key = 'q'
+	KeyR Key = 'r'
+	KeyS Key = 's'
+	KeyT Key = 't'
+	KeyU Key = 'u'
+	KeyV Key = 'v'
+	KeyW Key = 'w'
+	KeyX Key = 'x'
+	KeyY Key = 'y'
+	KeyZ Key = 'z'
+)
+
 // Key is a generic value for representing keys, and especially special
 // keys (function keys, cursor movement keys, etc.)  For normal keys, like
 // ASCII letters, we use KeyRune, and then expect the application to
@@ -351,6 +578,8 @@ const (
 	KeyCancel
 	KeyPrint
 	KeyPause
+	// KeyBacktab is used for legacy Shift-Tab reporting.  In advanced key
+	// reporting mode, Shift-Tab is reported as KeyTab with ModShift instead.
 	KeyBacktab
 	KeyF1
 	KeyF2
@@ -420,6 +649,11 @@ const (
 	KeyCapsLock
 	KeyScrollLock
 	KeyNumLock
+	KeyShift
+	KeyCtrl
+	KeyAlt
+	KeyMeta
+	KeyHyper
 )
 
 const (
@@ -432,6 +666,10 @@ const (
 // rune (lower case) and control modifier.  If the shift key
 // or other modifiers are present then these will *NOT* be reported,
 // but reported instead as KeyRune.
+//
+// Note that these are not reported in advanced key reporting mode.
+// Instead, for advanced keys, expect KeyRune and a modifier with the
+// associated rune to be sent.
 const (
 	KeyCtrlA Key = iota + 65
 	KeyCtrlB
@@ -466,6 +704,10 @@ const (
 
 // These are the defined ASCII values for key codes.  They generally match
 // with KeyCtrl values.
+//
+// Most of these will not be reported in advanced key reporting mode, as they
+// are not possible to type directly. Some notable exceptions are KeyESC, KeyBS,
+// KeyTAB, and KeyCR, which have aliases below.
 const (
 	KeyNUL Key = iota
 	KeySOH
diff --git a/vendor/github.com/gdamore/tcell/v3/mouse.go b/vendor/github.com/gdamore/tcell/v3/mouse.go
index c78092c1d..683ae0243 100644
--- a/vendor/github.com/gdamore/tcell/v3/mouse.go
+++ b/vendor/github.com/gdamore/tcell/v3/mouse.go
@@ -49,8 +49,9 @@ func (ev *EventMouse) Modifiers() ModMask {
 	return ev.mod
 }
 
-// Position returns the mouse position in character cells.  The origin
-// 0, 0 is at the upper left corner.
+// Position returns the mouse position.  The origin 0, 0 is at the upper
+// left corner.  The unit is character cells unless the screen was started
+// with MousePixelEvents, in which case the unit is terminal pixels.
 func (ev *EventMouse) Position() (int, int) {
 	return ev.x, ev.y
 }
diff --git a/vendor/github.com/gdamore/tcell/v3/screen.go b/vendor/github.com/gdamore/tcell/v3/screen.go
index f8c93e5c4..553709c4d 100644
--- a/vendor/github.com/gdamore/tcell/v3/screen.go
+++ b/vendor/github.com/gdamore/tcell/v3/screen.go
@@ -249,6 +249,9 @@ type Screen interface {
 	// supports it.  Right now only terminals supporting OSC 777 support this.
 	ShowNotification(title string, body string)
 
+	// KeyboardProtocol returns the keyboard protocol currently in use.
+	KeyboardProtocol() KeyProtocol
+
 	// Terminal returns the terminal name and version if known.  If either of these
 	// are unknown, then empty strings are returned in their place.  This is intended
 	// to facilitate debug, and also applications that wish to enable very specific
@@ -260,7 +263,8 @@ var overrideScreen chan Screen
 var overrideOnce sync.Once
 
 // NewScreen returns a default Screen suitable for the user's terminal environment.
-func NewScreen() (Screen, error) {
+// Any options are passed through to NewTerminfoScreen.
+func NewScreen(opts ...TerminfoScreenOption) (Screen, error) {
 
 	// Allow an application (presumably test code) to inject a replacement default
 	// screen.  This could also be used to create shims for things like nesting screens.
@@ -270,7 +274,7 @@ func NewScreen() (Screen, error) {
 	default:
 	}
 
-	if s, e := NewTerminfoScreen(); s != nil {
+	if s, e := NewTerminfoScreen(opts...); s != nil {
 		return s, nil
 	} else {
 		return nil, e
@@ -296,6 +300,15 @@ const (
 	MouseButtonEvents = MouseFlags(1) // Click events only
 	MouseDragEvents   = MouseFlags(2) // Click-drag events (includes button events)
 	MouseMotionEvents = MouseFlags(4) // All mouse events (includes click and drag events)
+	// MousePixelEvents requests that mouse coordinates be reported in
+	// terminal pixels rather than character cells (xterm SGR-Pixel mode,
+	// CSI ?1016h). It is a modifier on the other mouse flags: at least one
+	// of MouseButtonEvents, MouseDragEvents, or MouseMotionEvents must also
+	// be set for any events to be delivered. When this mode is active,
+	// EventMouse.Position() returns coordinates in pixels; the application
+	// is responsible for mapping those to its own grid (e.g. via the
+	// terminal's reported cell-pixel size).
+	MousePixelEvents = MouseFlags(8)
 )
 
 // CursorStyle represents a given cursor style, which can include the shape and
@@ -345,6 +358,7 @@ type screenImpl interface {
 	GetClipboard()
 	HasClipboard() bool
 	ShowNotification(string, string)
+	KeyboardProtocol() KeyProtocol
 	Terminal() (string, string)
 
 	// Following methods are not part of the Screen api, but are used for interaction with
@@ -382,16 +396,19 @@ func (b *baseScreen) Put(x int, y int, str string, style Style) (remain string,
 func (b *baseScreen) PutStrStyled(x int, y int, str string, style Style) {
 	cells := b.GetCells()
 	b.Lock()
+	defer b.Unlock()
 	cols, rows := cells.Size()
+	if cells.sanitizeContent {
+		str = stripOSCControlsIfNeeded(str)
+	}
 	width := 0
 	for str != "" && x < cols && y < rows {
-		str, width = cells.Put(x, y, str, style)
+		str, width = cells.put(x, y, str, style)
 		if width == 0 {
 			break
 		}
 		x += width
 	}
-	defer b.Unlock()
 }
 
 func (b *baseScreen) PutStr(x, y int, str string) {
diff --git a/vendor/github.com/gdamore/tcell/v3/style.go b/vendor/github.com/gdamore/tcell/v3/style.go
index 8aedd97db..7f629da3f 100644
--- a/vendor/github.com/gdamore/tcell/v3/style.go
+++ b/vendor/github.com/gdamore/tcell/v3/style.go
@@ -43,6 +43,7 @@ type urlInfo struct {
 	id  string
 }
 
+// stripOSCControls removes control bytes that can terminate OSC payloads early.
 func stripOSCControls(s string) string {
 	var b strings.Builder
 	b.Grow(len(s))
@@ -68,6 +69,18 @@ func stripOSCControls(s string) string {
 	return b.String()
 }
 
+// stripOSCControlsIfNeeded returns the original string when it contains no
+// control bytes and only allocates when stripping is required.
+func stripOSCControlsIfNeeded(s string) string {
+	for i := 0; i < len(s); i++ {
+		c := s[i]
+		if c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f) {
+			return stripOSCControls(s)
+		}
+	}
+	return s
+}
+
 // StyleDefault represents a default style, based upon the context.
 // It is the zero value.
 var StyleDefault Style
@@ -102,10 +115,15 @@ func (s Style) setAttrs(attrs AttrMask, on bool) Style {
 }
 
 // Normal returns the style with all attributes disabled.
+// Colors are preserved, as are hyperlinks.  (Underline color
+// will also be preserved, but no underline is currently shown.
+// Apart from color, the underline style is reset as well.)
 func (s Style) Normal() Style {
 	return Style{
-		fg: s.fg,
-		bg: s.bg,
+		fg:      s.fg,
+		bg:      s.bg,
+		ulColor: s.ulColor,
+		url:     s.url,
 	}
 }
 
@@ -227,10 +245,13 @@ func (s Style) GetAttributes() AttrMask {
 func (s Style) Url(url string) Style {
 
 	s2 := s
-	s2.url = &urlInfo{url: stripOSCControls(url)}
+	s2.url = &urlInfo{url: stripOSCControlsIfNeeded(url)}
 	if s.url != nil {
 		s2.url.id = s.url.id
 	}
+	if s2.url.url == "" && s2.url.id == "" {
+		s2.url = nil
+	}
 	return s2
 }
 
@@ -240,12 +261,16 @@ func (s Style) Url(url string) Style {
 // were one Url, even if it spans multiple lines.
 func (s Style) UrlId(id string) Style {
 	s2 := s
-	s2.url = &urlInfo{
-		id: "id=" + stripOSCControls(id),
+	s2.url = &urlInfo{}
+	if id = stripOSCControlsIfNeeded(id); id != "" {
+		s2.url.id = "id=" + id
 	}
 	if s.url != nil {
 		s2.url.url = s.url.url
 	}
+	if s2.url.url == "" && s2.url.id == "" {
+		s2.url = nil
+	}
 	return s2
 }
 
diff --git a/vendor/github.com/gdamore/tcell/v3/tscreen.go b/vendor/github.com/gdamore/tcell/v3/tscreen.go
index 3ff2c85bb..2fa180f63 100644
--- a/vendor/github.com/gdamore/tcell/v3/tscreen.go
+++ b/vendor/github.com/gdamore/tcell/v3/tscreen.go
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-//go:build !js && !wasm
-// +build !js,!wasm
+//go:build (!js && !wasm) || (js && wasm)
+// +build !js,!wasm js,wasm
 
 package tcell
 
@@ -83,6 +83,53 @@ func (o OptAltScreen) apply(t *tScreen) {
 	t.altScreen = bool(o)
 }
 
+// OptSanitizeContent enables stripping control characters from content passed
+// to Put and PutStr. This is safer, but a little slower than leaving content
+// unsanitized.
+type OptSanitizeContent bool
+
+func (o OptSanitizeContent) apply(t *tScreen) {
+	t.cells.sanitizeContent = bool(o)
+}
+
+// OptAdvancedKeys enables richer key reporting where supported.  In this mode
+// key events may include release state, repeat counts, and physical keys, and
+// ASCII control letters are reported as KeyRune with ModCtrl instead of
+// KeyCtrlA through KeyCtrlZ.  Shift-Tab is reported as KeyTab with ModShift,
+// rather than KeyBacktab.
+type OptAdvancedKeys bool
+
+func (o OptAdvancedKeys) apply(t *tScreen) {
+	t.advancedKeys = bool(o)
+}
+
+// OptKeyboardProtocol forces the keyboard reporting protocol instead of using
+// startup negotiation. The zero value forces legacy keyboard reporting.
+type OptKeyboardProtocol KeyProtocol
+
+func (o OptKeyboardProtocol) apply(t *tScreen) {
+	t.forceKeyboardProtocol(KeyProtocol(o))
+}
+
+// OptNegotiation controls whether terminal capabilities are negotiated during
+// startup. The default is true.
+type OptNegotiation bool
+
+func (o OptNegotiation) apply(t *tScreen) {
+	t.negotiate = bool(o)
+}
+
+// OptControlStringLimit sets the maximum inbound control-string payload size
+// accepted from the terminal before the parser drops the sequence. This limits
+// OSC and XDA strings, including OSC 52 clipboard strings; OSC 52 is the
+// protocol used for writing clipboard data through the terminal. The default is
+// 64 KiB; a value of 0 disables the limit.
+type OptControlStringLimit int
+
+func (o OptControlStringLimit) apply(t *tScreen) {
+	t.controlStringLimit = max(int(o), 0)
+}
+
 // Some terminal escapes that are basically universal.
 // We would really like to be able to use private mode queries for some of
 // these but generally we've found that support for queries is not always present,
@@ -132,6 +179,7 @@ const (
 	notifyDesktop777  = "\x1b]777;notify;%s;%s\x1b\\"       // Most commonly supported
 	queryKittyKbd     = "\x1b[?u"                           // Query for Kitty keyboard support
 	enableKittyKbd    = "\x1b[=1u"                          // Technically this pushes
+	enableKittyKbdAdv = "\x1b[=15u"                         // disambiguation, events, alternate keys, all keys
 	disableKittyKbd   = "\x1b[=0u"                          // Technically this means pop previous mode
 	queryXTermKbd     = "\x1b[?4m"                          // Query for XTerm modify other keys support
 	enableXTermKbd    = "\x1b[>4;2m"                        // Enable modify other keys protocol
@@ -143,7 +191,12 @@ const (
 // is presumed, at least on UNIX hosts. (Windows hosts will typically fail this
 // call altogether.)
 func NewTerminfoScreenFromTty(tty Tty, opts ...TerminfoScreenOption) (Screen, error) {
-	t := &tScreen{tty: tty, altScreen: true}
+	t := &tScreen{
+		tty:                tty,
+		altScreen:          true,
+		negotiate:          true,
+		controlStringLimit: defaultControlStringLimit,
+	}
 
 	t.prepareCursorStyles()
 	t.prepareExtendedOSC()
@@ -160,71 +213,77 @@ func NewTerminfoScreenFromTty(tty Tty, opts ...TerminfoScreenOption) (Screen, er
 
 // tScreen represents a screen backed by a terminfo implementation.
 type tScreen struct {
-	tty           Tty
-	h             int
-	w             int
-	fini          bool
-	cells         CellBuffer
-	buffering     bool // true if we are collecting writes to buf instead of sending directly to out
-	buf           bytes.Buffer
-	curstyle      Style
-	style         Style
-	resizeQ       chan bool
-	quit          chan struct{}
-	keyQ          chan []byte
-	cx            int
-	cy            int
-	cls           bool // clear screen
-	cursorx       int
-	cursory       int
-	acs           map[rune]string
-	charset       string
-	encoder       transform.Transformer
-	decoder       transform.Transformer
-	fallback      map[rune]string
-	ncolor        int
-	colors        map[color.Color]color.Color
-	palette       []color.Color
-	truecolor     bool
-	noColor       bool
-	legacy        bool
-	hasClipboard  bool // true if OSC 52 reported via DA1
-	finiOnce      sync.Once
-	enterUrl      string
-	exitUrl       string
-	setWinSize    string
-	cursorStyles  map[CursorStyle]string
-	cursorStyle   CursorStyle
-	cursorColor   color.Color
-	cursorRGB     string
-	cursorFg      string
-	stopQ         chan struct{}
-	eventQ        chan Event
-	initQ         chan Event
-	initted       bool
-	running       bool
-	startTime     time.Time
-	wg            sync.WaitGroup
-	mouseFlags    MouseFlags
-	pasteEnabled  bool
-	focusEnabled  bool
-	setTitle      string
-	saveTitle     string
-	restoreTitle  string
-	title         string
-	setClipboard  string
-	notifyDesktop string
-	termName      string
-	termVers      string
-	term          string // value from $TERM
-	altScreen     bool
-	inlineResize  bool
-	haveMouse     bool
-	haveMouseSgr  bool
-	haveKittyKbd  bool
-	haveWin32Kbd  bool
-	haveXTermKbd  bool
-	input         *inputParser
+	tty                Tty
+	h                  int
+	w                  int
+	fini               bool
+	cells              CellBuffer
+	buffering          bool // true if we are collecting writes to buf instead of sending directly to out
+	buf                bytes.Buffer
+	curstyle           Style
+	style              Style
+	resizeQ            chan bool
+	quit               chan struct{}
+	keyQ               chan []byte
+	cx                 int
+	cy                 int
+	cls                bool // clear screen
+	cursorx            int
+	cursory            int
+	acs                map[rune]string
+	charset            string
+	encoder            transform.Transformer
+	decoder            transform.Transformer
+	fallback           map[rune]string
+	ncolor             int
+	colors             map[color.Color]color.Color
+	palette            []color.Color
+	truecolor          bool
+	noColor            bool
+	legacy             bool
+	hasClipboard       bool // true if OSC 52 reported via DA1
+	finiOnce           sync.Once
+	enterUrl           string
+	exitUrl            string
+	setWinSize         string
+	cursorStyles       map[CursorStyle]string
+	cursorStyle        CursorStyle
+	cursorColor        color.Color
+	cursorRGB          string
+	cursorFg           string
+	stopQ              chan struct{}
+	eventQ             chan Event
+	initQ              chan Event
+	initted            bool
+	running            bool
+	startTime          time.Time
+	wg                 sync.WaitGroup
+	mouseFlags         MouseFlags
+	pasteEnabled       bool
+	focusEnabled       bool
+	setTitle           string
+	saveTitle          string
+	restoreTitle       string
+	title              string
+	setClipboard       string
+	notifyDesktop      string
+	termName           string
+	termVers           string
+	term               string // value from $TERM
+	altScreen          bool
+	inlineResize       bool
+	haveMouse          bool
+	haveMouseSgr       bool
+	haveKittyKbd       bool
+	haveWin32Kbd       bool
+	haveXTermKbd       bool
+	forcedKbd          KeyProtocol
+	forceKbd           bool
+	negotiate          bool
+	mouseDisabled      bool
+	advancedKeys       bool
+	controlStringLimit int
+	input              *inputParser
 	sync.Mutex
 }
 
@@ -232,6 +291,69 @@ func (t *tScreen) useAltScreen() bool {
 	return t.altScreen && os.Getenv("TCELL_ALTSCREEN") != "disable"
 }
 
+func validKeyboardProtocol(p KeyProtocol) bool {
+	switch p {
+	case LegacyKeyboard, KittyKeyboard, Win32Keyboard, XTermKeyboard:
+		return true
+	default:
+		return false
+	}
+}
+
+func parseKeyboardProtocol(s string) (KeyProtocol, bool) {
+	switch s {
+	case "legacy":
+		return LegacyKeyboard, true
+	case "kitty":
+		return KittyKeyboard, true
+	case "win32":
+		return Win32Keyboard, true
+	case "xterm":
+		return XTermKeyboard, true
+	default:
+		return LegacyKeyboard, false
+	}
+}
+
+func (t *tScreen) forceKeyboardProtocol(p KeyProtocol) bool {
+	if !validKeyboardProtocol(p) {
+		return false
+	}
+	t.forcedKbd = p
+	t.forceKbd = true
+	return true
+}
+
+func (t *tScreen) applyKeyboardProtocolOverride() {
+	if !t.forceKbd {
+		return
+	}
+	t.haveKittyKbd = t.forcedKbd == KittyKeyboard
+	t.haveWin32Kbd = t.forcedKbd == Win32Keyboard
+	t.haveXTermKbd = t.forcedKbd == XTermKeyboard
+}
+
+func (t *tScreen) applyEnvironmentOverrides() {
+	switch os.Getenv("TCELL_KEYBOARD_PROTOCOL") {
+	case "auto":
+		t.forceKbd = false
+	case "":
+	default:
+		if p, ok := parseKeyboardProtocol(os.Getenv("TCELL_KEYBOARD_PROTOCOL")); ok {
+			t.forceKeyboardProtocol(p)
+		}
+	}
+
+	switch os.Getenv("TCELL_NEGOTIATE") {
+	case "auto":
+		t.negotiate = true
+	case "disable":
+		t.negotiate = false
+	}
+
+	t.mouseDisabled = os.Getenv("TCELL_MOUSE") == "disable"
+}
+
 func (t *tScreen) Init() error {
 	if e := t.initialize(); e != nil {
 		return e
@@ -307,11 +429,15 @@ func (t *tScreen) Init() error {
 		t.legacy = true
 	}
 
+	t.applyEnvironmentOverrides()
+
 	t.initted = false
 	t.quit = make(chan struct{})
 	t.initQ = make(chan Event, 32)
 	t.eventQ = make(chan Event, 128)
 	t.input = newInputParser(t.filterEvents())
+	t.input.advanced = t.advancedKeys
+	t.input.controlStringMax = t.controlStringLimit
 
 	t.Lock()
 	t.cx = -1
@@ -480,6 +606,9 @@ func (t *tScreen) Fini() {
 }
 
 func (t *tScreen) finish() {
+	t.Lock()
+	t.fini = true
+	t.Unlock()
 	close(t.quit)
 	t.finalize()
 }
@@ -664,6 +793,13 @@ func (t *tScreen) emitUrl(u urlInfo) {
 	}
 }
 
+// urlNeedsEmission reports whether a hyperlink transition has any wire effect.
+// Url ids can be staged before the Url itself, and id-only transitions have no
+// OSC 8 representation of their own.
+func urlNeedsEmission(oldUrl, newUrl urlInfo) bool {
+	return oldUrl != newUrl && (oldUrl.url != "" || newUrl.url != "")
+}
+
 func (t *tScreen) drawCell(x, y int) int {
 
 	str, style, width := t.cells.Get(x, y)
@@ -697,8 +833,8 @@ func (t *tScreen) drawCell(x, y int) int {
 		if style.url != nil {
 			newUrl = *style.url
 		}
-		// URL string can be long, so don't send it unless we really need to
-		if newUrl != oldUrl {
+		// URL string can be long, so don't send it unless we really need to.
+		if urlNeedsEmission(oldUrl, newUrl) {
 			t.emitUrl(newUrl)
 		}
 
@@ -746,6 +882,10 @@ func (t *tScreen) drawCell(x, y int) int {
 
 func (t *tScreen) ShowCursor(x, y int) {
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	t.cursorx = x
 	t.cursory = y
 	t.Unlock()
@@ -753,6 +893,10 @@ func (t *tScreen) ShowCursor(x, y int) {
 
 func (t *tScreen) SetCursor(cs CursorStyle, cc Color) {
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	t.cursorStyle = cs
 	t.cursorColor = cc
 	t.Unlock()
@@ -876,6 +1020,10 @@ func (t *tScreen) draw() {
 		}
 	}
 
+	if t.curstyle.url != nil && t.curstyle.url.url != "" {
+		t.emitUrl(urlInfo{})
+	}
+
 	// restore the cursor
 	t.showCursor()
 
@@ -894,6 +1042,10 @@ func (t *tScreen) EnableMouse(flags ...MouseFlags) {
 	}
 
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	t.mouseFlags = f
 	t.enableMouse(f)
 	t.Unlock()
@@ -912,7 +1064,10 @@ func (t *tScreen) enableMouse(f MouseFlags) {
 	// so we enable the mouse unconditionally unless we get a report
 	// that says we have mouse, but not SGR mouse.  This is suboptimal, but
 	// a concession forced by the sorry state of terminal emulators.
-	if t.haveMouse && !t.haveMouseSgr {
+	if t.mouseDisabled {
+		f = 0
+	}
+	if f != 0 && t.haveMouse && !t.haveMouseSgr {
 		return
 	}
 
@@ -921,6 +1076,10 @@ func (t *tScreen) enableMouse(f MouseFlags) {
 	t.Print(vt.PmMouseDrag.Disable())
 	t.Print(vt.PmMouseMotion.Disable())
 	t.Print(vt.PmMouseSgr.Disable())
+	t.Print(vt.PmMouseSgrPixel.Disable())
+
+	pixel := f&MousePixelEvents != 0
+	t.input.SetPixelMouse(pixel)
 
 	if f&(MouseButtonEvents|MouseDragEvents|MouseMotionEvents) != 0 {
 		t.Print(vt.PmMouseButton.Enable())
@@ -932,12 +1091,20 @@ func (t *tScreen) enableMouse(f MouseFlags) {
 		t.Print(vt.PmMouseMotion.Enable())
 	}
 	if f&(MouseButtonEvents|MouseDragEvents|MouseMotionEvents) != 0 {
-		t.Print(vt.PmMouseSgr.Enable())
+		if pixel {
+			t.Print(vt.PmMouseSgrPixel.Enable())
+		} else {
+			t.Print(vt.PmMouseSgr.Enable())
+		}
 	}
 }
 
 func (t *tScreen) DisableMouse() {
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	t.mouseFlags = 0
 	t.enableMouse(0)
 	t.Unlock()
@@ -945,6 +1112,10 @@ func (t *tScreen) DisableMouse() {
 
 func (t *tScreen) EnablePaste() {
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	t.pasteEnabled = true
 	t.enablePasting(true)
 	t.Unlock()
@@ -952,6 +1123,10 @@ func (t *tScreen) EnablePaste() {
 
 func (t *tScreen) DisablePaste() {
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	t.pasteEnabled = false
 	t.enablePasting(false)
 	t.Unlock()
@@ -971,6 +1146,10 @@ func (t *tScreen) enablePasting(on bool) {
 
 func (t *tScreen) EnableFocus() {
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	t.focusEnabled = true
 	t.enableFocusReporting()
 	t.Unlock()
@@ -978,6 +1157,10 @@ func (t *tScreen) EnableFocus() {
 
 func (t *tScreen) DisableFocus() {
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	t.focusEnabled = false
 	t.disableFocusReporting()
 	t.Unlock()
@@ -1189,6 +1372,11 @@ func (t *tScreen) UnregisterRuneFallback(orig rune) {
 }
 
 func (t *tScreen) SetSize(w, h int) {
+	t.Lock()
+	defer t.Unlock()
+	if t.fini {
+		return
+	}
 	if t.setWinSize != "" {
 		t.Printf(t.setWinSize, w, h)
 	}
@@ -1199,24 +1387,86 @@ func (t *tScreen) SetSize(w, h int) {
 func (t *tScreen) Resize(int, int, int, int) {}
 
 func (t *tScreen) Suspend() error {
-	t.disengage()
+	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return nil
+	}
+	finish := t.disengageStart()
+	t.Unlock()
+	if finish {
+		t.disengageFinish()
+	}
 	return nil
 }
 
 func (t *tScreen) Resume() error {
-	return t.engage()
+	t.Lock()
+	defer t.Unlock()
+	if t.fini {
+		return nil
+	}
+	return t.engageLocked()
 }
 
 func (t *tScreen) Tty() (Tty, bool) {
 	return t.tty, true
 }
 
+func (t *tScreen) applyKnownTerminalProfile(goos, termProgram string) bool {
+	switch termProgram {
+	case "Apple_Terminal":
+		// macOS Terminal.app cannot handle the startup queries, but it does
+		// support modern mouse reporting.
+		t.haveMouse = true
+		t.haveMouseSgr = true
+		t.termName = "Terminal.app"
+		t.termVers = os.Getenv("TERM_PROGRAM_VERSION")
+		return true
+	case "WezTerm":
+		// The WezTerm keyboard protocol to use is in theory driven by its
+		// own configuration, but we have found this unreliable because it
+		// does not mask unsupported capabilities.  Furthermore, on Windows
+		// builds the kitty protocol implementation is broken, while on other
+		// builds win32-input-mode is broken.  This is a best effort to make
+		// WezTerm work reasonably; our stronger advice is to choose another
+		// terminal program altogether.  This workaround will probably not
+		// apply to ssh sessions, as TERM_PROGRAM is not normally propagated.
+		if goos == "windows" {
+			t.haveWin32Kbd = true
+		} else {
+			t.haveKittyKbd = true
+			t.haveWin32Kbd = false
+		}
+		t.haveMouse = true
+		t.haveMouseSgr = true
+		t.initted = true
+		t.termName = "WezTerm"
+		t.termVers = os.Getenv("TERM_PROGRAM_VERSION")
+		return true
+	}
+	return false
+}
+
+func useVTWindowSizeQuery(goos string) bool {
+	return goos != "windows"
+}
+
+func useXTermKeyboardQuery(goos string) bool {
+	return goos != "windows"
+}
+
 // engage is used to place the terminal in raw mode and establish screen size, etc.
 // Think of this is as tcell "engaging" the clutch, as it's going to be driving the
 // terminal interface.
 func (t *tScreen) engage() error {
 	t.Lock()
 	defer t.Unlock()
+	return t.engageLocked()
+}
+
+// engageLocked is engage's implementation when t's lock is already held.
+func (t *tScreen) engageLocked() error {
 	if t.tty == nil {
 		return ErrNoScreen
 	}
@@ -1234,24 +1484,37 @@ func (t *tScreen) engage() error {
 	go t.mainLoop(stopQ)
 
 	if !t.initted {
-		t.Print(requestWindowSize)
 		// macOS Terminal.app is brain damaged
 		// https://garrett.damore.org/2025/12/macos-terminal-still-missing-mark-apple.html
 		// Eventually they'll hopefully fix this.  As the environment variable
 		// does not convey by default via ssh, remote sessions might see spurious characters
 		// emitted during startup.  See the blog post for alternatives.
-		if os.Getenv("TERM_PROGRAM") != "Apple_Terminal" {
+		if !t.applyKnownTerminalProfile(runtime.GOOS, os.Getenv("TERM_PROGRAM")) && t.negotiate {
+			if useVTWindowSizeQuery(runtime.GOOS) {
+				t.Print(requestWindowSize)
+			}
 			t.Print(vt.PmResizeReports.Query())
 			t.Print(vt.PmMouseButton.Query())
 			t.Print(vt.PmMouseSgr.Query())
-			t.Print(vt.PmWin32Input.Query())
-			t.Print(queryKittyKbd)
-			t.Print(queryXTermKbd)
+			if !t.forceKbd {
+				t.Print(vt.PmWin32Input.Query())
+				t.Print(queryKittyKbd)
+				if useXTermKeyboardQuery(runtime.GOOS) {
+					// XTerm's modifyOtherKeys mode is mainly useful for XTerm
+					// itself, and we do not use it on Windows.
+					t.Print(queryXTermKbd)
+				}
+			}
 			t.Print(requestExtAttr)
 		}
-		t.Print(requestPrimaryDA) // NB: MUST BE LAST
+		if !t.negotiate {
+			t.initted = true
+		} else if !t.initted {
+			t.Print(requestPrimaryDA) // NB: MUST BE LAST
+		}
 	}
 	t.processInitQ()
+	t.applyKeyboardProtocolOverride()
 	if t.useAltScreen() {
 		// Technically this may not be right, but every terminal we know about
 		// (even Wyse 60) uses this to enter the alternate screen buffer, and
@@ -1264,7 +1527,11 @@ func (t *tScreen) engage() error {
 	if t.haveWin32Kbd {
 		t.Print(vt.PmWin32Input.Enable())
 	} else if t.haveKittyKbd {
-		t.Print(enableKittyKbd)
+		if t.advancedKeys {
+			t.Print(enableKittyKbdAdv)
+		} else {
+			t.Print(enableKittyKbd)
+		}
 	} else if t.haveXTermKbd {
 		t.Print(enableXTermKbd)
 	}
@@ -1286,17 +1553,9 @@ func (t *tScreen) engage() error {
 	if t.title != "" && t.setTitle != "" {
 		t.Printf(t.setTitle, t.title)
 	}
-	if runtime.GOOS == "windows" {
-		// This workaround exists because of what we believe to be bugs in the
-		// interaction between ConPTY, the VT-Input layer, and some terminal emulators
-		// such as WezTerm.  Note that it is *not* needed for Windows Terminal, but
-		// should be benign there.  As another note, we have observed that at least Alacritty
-		// and WezTerm do not properly handle the primaryDA query on these platforms.
-		// (WezTerm performs much better when running a remote shell or on macOS.)
-		t.Print(enableKittyKbd)
-		t.Print(vt.PmWin32Input.Enable())
+	if t.negotiate && useVTWindowSizeQuery(runtime.GOOS) {
+		t.Print(requestWindowSize)
 	}
-	t.Print(requestWindowSize)
 
 	if t.inlineResize {
 		t.Print(vt.PmResizeReports.Enable())
@@ -1311,11 +1570,19 @@ func (t *tScreen) engage() error {
 // can take over the terminal interface.  This restores the TTY mode that was
 // present when the application was first started.
 func (t *tScreen) disengage() {
-
 	t.Lock()
+	finish := t.disengageStart()
+	t.Unlock()
+	if finish {
+		t.disengageFinish()
+	}
+}
+
+// disengageStart begins a disengage operation while t's lock is already held.
+// It returns true when disengageFinish must be called after releasing the lock.
+func (t *tScreen) disengageStart() bool {
 	if !t.running {
-		t.Unlock()
-		return
+		return false
 	}
 
 	t.running = false
@@ -1327,8 +1594,12 @@ func (t *tScreen) disengage() {
 	stopQ := t.stopQ
 	close(stopQ)
 	_ = t.tty.Drain()
-	t.Unlock()
+	return true
+}
 
+// disengageFinish completes a disengage operation after disengageStart has
+// released the running loops.
+func (t *tScreen) disengageFinish() {
 	// wait for everything to shut down
 	t.wg.Wait()
 
@@ -1357,7 +1628,6 @@ func (t *tScreen) disengage() {
 	// Hack for Windows.
 	if runtime.GOOS == "windows" {
 		t.Print(vt.PmWin32Input.Disable())
-		t.Print(disableKittyKbd)
 	}
 
 	// t.Print(t.disableCsiU)
@@ -1375,6 +1645,11 @@ func (t *tScreen) disengage() {
 
 // Beep emits a beep to the terminal.
 func (t *tScreen) Beep() error {
+	t.Lock()
+	defer t.Unlock()
+	if t.fini {
+		return nil
+	}
 	t.Print(string(byte(7)))
 	return nil
 }
@@ -1401,9 +1676,13 @@ func (t *tScreen) GetCells() *CellBuffer {
 
 func (t *tScreen) SetTitle(title string) {
 	t.Lock()
-	t.title = title
+	if t.fini {
+		t.Unlock()
+		return
+	}
+	t.title = stripOSCControlsIfNeeded(title)
 	if t.setTitle != "" && t.running {
-		t.Printf(t.setTitle, title)
+		t.Printf(t.setTitle, t.title)
 	}
 	t.Unlock()
 }
@@ -1411,6 +1690,10 @@ func (t *tScreen) SetTitle(title string) {
 func (t *tScreen) SetClipboard(data []byte) {
 	// Post binary data to the system clipboard.  It might be UTF-8, it might not be.
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	if t.setClipboard != "" {
 		encoded := base64.StdEncoding.EncodeToString(data)
 		t.Printf(t.setClipboard, encoded)
@@ -1420,6 +1703,10 @@ func (t *tScreen) SetClipboard(data []byte) {
 
 func (t *tScreen) GetClipboard() {
 	t.Lock()
+	if t.fini {
+		t.Unlock()
+		return
+	}
 	if t.setClipboard != "" {
 		t.Printf(t.setClipboard, "?")
 	}
@@ -1432,7 +1719,11 @@ func (t *tScreen) HasClipboard() bool {
 
 func (t *tScreen) ShowNotification(title string, body string) {
 	t.Lock()
-	t.Printf(t.notifyDesktop, title, body)
+	if t.fini {
+		t.Unlock()
+		return
+	}
+	t.Printf(t.notifyDesktop, stripOSCControlsIfNeeded(title), stripOSCControlsIfNeeded(body))
 	t.Unlock()
 }
 
@@ -1441,3 +1732,18 @@ func (t *tScreen) Terminal() (string, string) {
 	defer t.Unlock()
 	return t.termName, t.termVers
 }
+
+func (t *tScreen) KeyboardProtocol() KeyProtocol {
+	t.Lock()
+	defer t.Unlock()
+	if t.haveWin32Kbd {
+		return Win32Keyboard
+	}
+	if t.haveKittyKbd {
+		return KittyKeyboard
+	}
+	if t.haveXTermKbd {
+		return XTermKeyboard
+	}
+	return LegacyKeyboard
+}
diff --git a/vendor/github.com/gdamore/tcell/v3/tty/tty_win.go b/vendor/github.com/gdamore/tcell/v3/tty/tty_win.go
index 2bf9769d1..853c1e136 100644
--- a/vendor/github.com/gdamore/tcell/v3/tty/tty_win.go
+++ b/vendor/github.com/gdamore/tcell/v3/tty/tty_win.go
@@ -189,6 +189,10 @@ func (w *winTty) getConsoleInput() error {
 		rv, _, er := procGetNumberOfConsoleInputEvents.Call(
 			uintptr(w.in),
 			uintptr(unsafe.Pointer(&nrec)))
+		if rv == 0 {
+			return er
+		}
+
 		rec := make([]inputRecord, max(nrec, 1))
 		rv, _, er = procReadConsoleInput.Call(
 			uintptr(w.in),
@@ -205,22 +209,15 @@ func (w *winTty) getConsoleInput() error {
 			case keyEvent:
 				// we normally only expect to see ascii, but paste data may come in as UTF-16.
 				wc := rune(binary.LittleEndian.Uint16(ir.data[10:]))
-				if wc >= 0xD800 && wc <= 0xDBFF {
-					// if it was a high surrogate, which happens for pasted UTF-16,
-					// then save it until we get the low and can decode it.
-					w.surrogate = wc
-					continue
-				} else if wc >= 0xDC00 && wc <= 0xDFFF {
-					wc = utf16.DecodeRune(w.surrogate, wc)
-				}
-				w.surrogate = 0
-				for _, chr := range []byte(string(wc)) {
-					// We normally expect only to see ASCII (win32-input-mode),
-					// but apparently pasted data can arrive in UTF-16 here.
-					select {
-					case w.buf <- chr:
-					case <-w.stopQ:
-						break loop
+				for _, decoded := range decodeUTF16Rune(&w.surrogate, wc) {
+					for _, chr := range []byte(string(decoded)) {
+						// We normally expect only to see ASCII (win32-input-mode),
+						// but apparently pasted data can arrive in UTF-16 here.
+						select {
+						case w.buf <- chr:
+						case <-w.stopQ:
+							break loop
+						}
 					}
 				}
 
diff --git a/vendor/github.com/gdamore/tcell/v3/tty/utf16.go b/vendor/github.com/gdamore/tcell/v3/tty/utf16.go
new file mode 100644
index 000000000..0932b3851
--- /dev/null
+++ b/vendor/github.com/gdamore/tcell/v3/tty/utf16.go
@@ -0,0 +1,47 @@
+// Copyright 2026 The TCell Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package tty
+
+import (
+	"unicode/utf16"
+	"unicode/utf8"
+)
+
+// decodeUTF16Rune decodes one UTF-16 code unit at a time while preserving
+// malformed input as replacement characters instead of silently discarding it.
+func decodeUTF16Rune(surrogate *rune, wc rune) []rune {
+	switch {
+	case wc >= 0xD800 && wc <= 0xDBFF:
+		if *surrogate != 0 {
+			*surrogate = wc
+			return []rune{utf8.RuneError}
+		}
+		*surrogate = wc
+		return nil
+	case wc >= 0xDC00 && wc <= 0xDFFF:
+		if *surrogate == 0 {
+			return []rune{utf8.RuneError}
+		}
+		decoded := utf16.DecodeRune(*surrogate, wc)
+		*surrogate = 0
+		return []rune{decoded}
+	default:
+		if *surrogate != 0 {
+			*surrogate = 0
+			return []rune{utf8.RuneError, wc}
+		}
+		return []rune{wc}
+	}
+}
diff --git a/vendor/github.com/gdamore/tcell/v3/vt/emulate.go b/vendor/github.com/gdamore/tcell/v3/vt/emulate.go
index b2dd2129b..fbbb6d443 100644
--- a/vendor/github.com/gdamore/tcell/v3/vt/emulate.go
+++ b/vendor/github.com/gdamore/tcell/v3/vt/emulate.go
@@ -220,10 +220,25 @@ type Cell struct {
 	W int    // Display width (0, 1, or 2)
 }
 
+// EmulatorOpt configures an Emulator.
+type EmulatorOpt interface {
+	setEmulatorOpt(*emulator)
+}
+
+// EmulatorOpt8BitControls enables parsing of C1 controls, such as CSI and OSC,
+// when they are presented as raw 8-bit bytes or UTF-8 encoded C1 controls.
+// The default is to only accept the 7-bit ESC-prefixed forms.
+type EmulatorOpt8BitControls struct{}
+
+func (EmulatorOpt8BitControls) setEmulatorOpt(em *emulator) {
+	em.c1Allowed = true
+	em.c1Enabled = true
+}
+
 // NewEmulator creates an emulator instance on top of the given backend.
 // The input is relative to the emulator, so it receives data from the host,
 // whereas the emulator sends data to the application through the output.
-func NewEmulator(be Backend) Emulator {
+func NewEmulator(be Backend, opts ...EmulatorOpt) Emulator {
 	stopQ := make(chan bool)
 	defStyle := BaseStyle.WithFg(color.Silver).WithBg(color.Black)
 	em := &emulator{
@@ -249,6 +264,9 @@ func NewEmulator(be Backend) Emulator {
 		},
 		mouseReports: MouseDisabled,
 	}
+	for _, opt := range opts {
+		opt.setEmulatorOpt(em)
+	}
 	if _, ok := be.(Resizer); ok {
 		em.localModes[PmResizeReports] = ModeOff
 	}
@@ -297,7 +315,9 @@ type emulator struct {
 	pos            Coord
 	buffering      uint         // reference count - number of (re-entrant) buffering calls
 	autoWrap       bool         // next character will wrap (auto margin, deferred until char emitted)
-	sevenOnly      bool         // only allow 7-bit escapes (needed for KOI8, ShiftJIS, etc.)
+	c1Allowed      bool         // allow C1 controls in raw 8-bit and UTF-8 encodings
+	c1Enabled      bool         // C1 controls are currently enabled
+	c1Prefix       bool         // string parser has seen the first byte of a UTF-8 encoded C1 control
 	appKeyPad      bool         // use application key pad keys?
 	name           string       // name of this emulator (used for extended attributes)
 	vers           string       // version string of this emulator (used for extended attributes)
@@ -372,13 +392,13 @@ func (em *emulator) inbInit(b byte) {
 		return
 	}
 
-	// For 8-bit encodings, we treat these as Fe sequences.
-	// Basically the same as ESC followed by (b - 0x40).
-	// TODO: condition this so that we do not do this if
-	// the encoding cannot support it (UTF, 8859, and EUC encodings
-	// are all fine here, but others like ShiftJIS or KOI8 might not be).
-	if b >= 0x80 && b <= 0x9F && !em.sevenOnly {
-		em.inbEsc(b - 0x40)
+	// For C1 controls, the raw 8-bit form is the same as ESC followed by
+	// (b - 0x40). This is disabled by default because modern protocols
+	// generally treat these forms as insecure.
+	if b >= 0x80 && b <= 0x9f {
+		if em.c1Enabled {
+			em.inbEsc(b - 0x40)
+		}
 		return
 	}
 
@@ -529,6 +549,12 @@ func (em *emulator) inbNF(b byte) {
 		// case "(Q", "(9": // TODO: select G0 as French Canadian
 		// case "(R", "(f": // TODO: select G0 as French
 		// case "(Y": // TODO: select G0 as Italian
+	case " F": // S7C1T - send/use 7-bit C1 controls
+		em.c1Enabled = false
+	case " G": // S8C1T - send/use 8-bit C1 controls
+		if em.c1Allowed {
+			em.c1Enabled = true
+		}
 	}
 }
 
@@ -550,8 +576,17 @@ func (em *emulator) inbCSI(b byte) {
 
 // inbOSC handles bytes that are part of on OSC sequences (operating system command).
 func (em *emulator) inbOSC(b byte) {
+	if em.inbStringC1(b, em.processOSC) {
+		return
+	}
+
 	switch b {
-	case 0x9c, 0x07:
+	case 0x9c:
+		if em.c1Enabled {
+			em.inb = em.inbInit
+			em.processOSC()
+		}
+	case 0x07:
 		em.inb = em.inbInit
 		em.processOSC()
 	case '\\':
@@ -569,8 +604,16 @@ func (em *emulator) inbOSC(b byte) {
 
 // inbStr handles PM, SOS, and any other string we want to consume and discard.
 func (em *emulator) inbStr(b byte) {
+	if em.inbStringC1(b, nil) {
+		return
+	}
+
 	switch b {
-	case 0x9c, 0x07:
+	case 0x9c:
+		if em.c1Enabled {
+			em.inb = em.inbInit
+		}
+	case 0x07:
 		em.inb = em.inbInit
 	case '\\':
 		if buf := em.inBuf.Bytes(); len(buf) > 0 && buf[len(buf)-1] == 0x1b {
@@ -584,6 +627,27 @@ func (em *emulator) inbStr(b byte) {
 	}
 }
 
+func (em *emulator) inbStringC1(b byte, done func()) bool {
+	if em.c1Prefix {
+		em.c1Prefix = false
+		if b >= 0x80 && b <= 0x9f {
+			if em.c1Enabled && b == 0x9c {
+				em.inb = em.inbInit
+				if done != nil {
+					done()
+				}
+			}
+			return true
+		}
+		em.inBuf.WriteByte(0xc2)
+	}
+	if b == 0xc2 {
+		em.c1Prefix = true
+		return true
+	}
+	return false
+}
+
 // inbUTF handles continuation bytes for UTF-8 sequences.
 func (em *emulator) inbUTF(b byte) {
 	if b&0xC0 == 0x80 {
@@ -595,7 +659,13 @@ func (em *emulator) inbUTF(b byte) {
 			if err != nil {
 				em.beep()
 			} else {
-				em.putRune(r)
+				if r >= 0x80 && r <= 0x9f {
+					if em.c1Enabled {
+						em.inbEsc(byte(r) - 0x40)
+					}
+				} else {
+					em.putRune(r)
+				}
 			}
 		}
 	} else {
@@ -730,7 +800,7 @@ func (em *emulator) processSgr(str string) {
 		v, err := strconv.Atoi(word)
 		if err != nil {
 			// just swallow it for now
-			return
+			continue
 		}
 		switch v {
 		case 0:
@@ -2333,6 +2403,7 @@ func (em *emulator) ResizeEvent(size Coord) {
 func (em *emulator) applyResize(size Coord) {
 	// resize clobbers our content, until it is redrawn
 	em.size = size
+	em.tabStops = slices.DeleteFunc(em.tabStops, func(x Col) bool { return x >= em.size.X })
 	// resizing resets the margins
 	em.topMargin = 0
 	em.botMargin = em.size.Y - 1
diff --git a/vendor/github.com/gdamore/tcell/v3/vt/key.go b/vendor/github.com/gdamore/tcell/v3/vt/key.go
index 9b36e9d48..edbd158e7 100644
--- a/vendor/github.com/gdamore/tcell/v3/vt/key.go
+++ b/vendor/github.com/gdamore/tcell/v3/vt/key.go
@@ -15,7 +15,7 @@
 package vt
 
 // BaseKey is the Kitty protocol base key. These are kitty's representation of a scan code.
-// As the Kitty protocol is likely the extended keyboard protocol we care about, we use
+// As the Kitty protocol is likely the extended keyboard protocol we care most about, we use
 // this as the primary reporting mechanism. (It also helps that this may provide an easier
 // fallback for implementations that don't have raw scan codes and are willing to assume an
 // ANSI layout.)
@@ -198,7 +198,7 @@ func (k Key) ScanCode() ScanCode {
 }
 
 // WinVK represents a windows virtual key code.
-// These are similar to base keys, but a multiple scanned key codes
+// These are similar to base keys, but multiple scanned key codes
 // may result in the same virtual key.  This can also be sensitive to
 // the keyboard layout.
 type WinVK rune
@@ -346,7 +346,7 @@ const (
 
 var baseKeys map[Key]BaseKey
 
-// KittyBase returns the corresponding Kitty "base" key for the given USB cod.
+// KittyBase returns the corresponding Kitty "base" key for the given USB code.
 // If no corresponding value can be found, then zero is returned.  Note that
 // some keys (such as F1) are valid, and recognized by Kitty, but do not use the
 // base key encoding because they use another reporting format.
@@ -462,15 +462,16 @@ func init() {
 		KeyRAlt:         57449,
 		KeyRMeta:        57450,
 
-		// KeyHiragana:   0, // TBD
-		// KeyConvert:    0, // TBD
-		// KeyNonConvert: 0, // TD
+		// KeyHiragana:   0, // Later
+		// KeyConvert:    0, // Later
+		// KeyNonConvert: 0, // Later
 
 		// Windows uses a bunch of HID usages from
 		// the consumer page (0x0c) for media playback, and
 		// other applications. We just ignore them.
 	}
 
+	// Scan codes used by Windows.
 	scanCodes = map[Key]ScanCode{
 		KeyA:            0x1e,
 		KeyB:            0x30,
diff --git a/vendor/github.com/gdamore/tcell/v3/vt/mock.go b/vendor/github.com/gdamore/tcell/v3/vt/mock.go
index 156013ea1..71b88ed57 100644
--- a/vendor/github.com/gdamore/tcell/v3/vt/mock.go
+++ b/vendor/github.com/gdamore/tcell/v3/vt/mock.go
@@ -247,13 +247,16 @@ func NewMockTerm(opts ...MockOpt) MockTerm {
 	mt := &mockTerm{}
 	mt.mb = NewMockBackend(opts...)
 	var be MockBackend = mt.mb
+	emOpts := []EmulatorOpt{}
 	for _, o := range opts {
 		switch o.(type) {
 		case MockOptNoBlit:
 			be = &noMockBlit{be, struct{}{}}
+		case MockOpt8BitControls:
+			emOpts = append(emOpts, EmulatorOpt8BitControls{})
 		}
 	}
-	mt.em = NewEmulator(be)
+	mt.em = NewEmulator(be, emOpts...)
 	mt.em.SetId("TCellMock", "1.0")
 	mt.ks = &KeyboardState{}
 	return mt
@@ -644,6 +647,12 @@ type MockOptNoBlit struct{}
 
 func (MockOptNoBlit) SetMockOpt(mb *mockBackend) {}
 
+// MockOpt8BitControls enables raw 8-bit and UTF-8 encoded C1 controls in the
+// emulator. The default is to accept only 7-bit ESC-prefixed controls.
+type MockOpt8BitControls struct{}
+
+func (MockOpt8BitControls) SetMockOpt(mb *mockBackend) {}
+
 // NewMockBackend returns a MockBackend modified by the given options.
 // The default is a fully featured 256-color backend with initial size 80x24.
 func NewMockBackend(options ...MockOpt) MockBackend {
diff --git a/vendor/github.com/gdamore/tcell/v3/wscreen.go b/vendor/github.com/gdamore/tcell/v3/wscreen.go
index 53dbfc064..d4fb6ba62 100644
--- a/vendor/github.com/gdamore/tcell/v3/wscreen.go
+++ b/vendor/github.com/gdamore/tcell/v3/wscreen.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The TCell Authors
+// Copyright 2026 The TCell Authors
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -19,611 +19,206 @@ package tcell
 
 import (
 	"errors"
-	"fmt"
+	"io"
 	"sync"
 	"syscall/js"
-	"unicode/utf8"
 
 	"github.com/gdamore/tcell/v3/tty"
 )
 
-// NewTerminfoScreen gets a screen.  The options are ignored for this platform.
-func NewTerminfoScreen(_ ...TerminfoScreenOption) (Screen, error) {
-	t := &wScreen{}
-	t.fallback = make(map[rune]string)
-
-	return &baseScreen{screenImpl: t}, nil
-}
-
-func NewTerminfoScreenFromTty(_ tty.Tty, _ ...TerminfoScreenOption) (Screen, error) {
-	// TODO: When we want to support testing webasm, we'll have to change this
-	// to use a MockTerm.  That will be appropriate when we switch to xterm.js.
-	// The options are ignored for this platform.
-	return nil, errors.New("not implemented")
-}
-
-type TerminfoScreenOption interface{ apply(*wScreen) }
-type OptColors int
-type OptTerm string
-type OptAltScreen bool
-
-func (OptColors) apply(*wScreen)    {}
-func (OptTerm) apply(*wScreen)      {}
-func (OptAltScreen) apply(*wScreen) {}
-
-type wScreen struct {
-	w, h  int
-	style Style
-	cells CellBuffer
-
-	running      bool
-	clear        bool
-	flagsPresent bool
-	pasteEnabled bool
-	mouseFlags   MouseFlags
-
-	cursorStyle CursorStyle
-
-	quit     chan struct{}
-	evch     chan Event
-	fallback map[rune]string
-	finiOnce sync.Once
-
-	sync.Mutex
-}
-
-func (t *wScreen) Init() error {
-	t.w, t.h = 80, 24 // default for html as of now
-	t.evch = make(chan Event, 10)
-	t.quit = make(chan struct{})
-
-	t.Lock()
-	t.running = true
-	t.style = StyleDefault
-	t.cells.Resize(t.w, t.h)
-	t.Unlock()
-
-	js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent))
-	js.Global().Set("onMouseClick", js.FuncOf(t.unset))
-	js.Global().Set("onMouseMove", js.FuncOf(t.unset))
-	js.Global().Set("onFocus", js.FuncOf(t.unset))
-
+// initialize installs the browser-backed TTY used by tScreen on js/wasm.
+func (t *tScreen) initialize() error {
+	if t.tty == nil {
+		t.tty = newBrowserTty()
+	}
+	if t.term == "" {
+		t.term = "ghostty-truecolor"
+	}
 	return nil
 }
 
-func (t *wScreen) Fini() {
-	t.finiOnce.Do(func() {
-		close(t.quit)
-	})
-}
-
-func (t *wScreen) SetStyle(style Style) {
-	t.Lock()
-	t.style = style
-	t.Unlock()
-}
-
-// paletteColor gives a more natural palette color actually matching
-// typical XTerm.  We might in the future want to permit styling these
-// via CSS.
-
-var palette = map[Color]int32{
-	ColorBlack:   0x000000,
-	ColorMaroon:  0xcd0000,
-	ColorGreen:   0x00cd00,
-	ColorOlive:   0xcdcd00,
-	ColorNavy:    0x0000ee,
-	ColorPurple:  0xcd00cd,
-	ColorTeal:    0x00cdcd,
-	ColorSilver:  0xe5e5e5,
-	ColorGray:    0x7f7f7f,
-	ColorRed:     0xff0000,
-	ColorLime:    0x00ff00,
-	ColorYellow:  0xffff00,
-	ColorBlue:    0x5c5cff,
-	ColorFuchsia: 0xff00ff,
-	ColorAqua:    0x00ffff,
-	ColorWhite:   0xffffff,
-}
-
-func paletteColor(c Color) int32 {
-	if c.IsRGB() {
-		return int32(c & 0xffffff)
-	}
-	if c >= ColorBlack && c <= ColorWhite {
-		return palette[c]
-	}
-	return c.Hex()
-}
-
-func (t *wScreen) drawCell(x, y int) int {
-	str, style, width := t.cells.Get(x, y)
-
-	if !t.cells.Dirty(x, y) {
-		return width
-	}
-
-	if style == StyleDefault {
-		style = t.style
-	}
-
-	fg, bg := paletteColor(style.fg), paletteColor(style.bg)
-	if fg == -1 {
-		fg = 0xe5e5e5
-	}
-	if bg == -1 {
-		bg = 0x000000
-	}
-	us, uc := style.ulStyle, paletteColor(style.ulColor)
-	if uc == -1 {
-		uc = 0x000000
-	}
-
-	t.cells.SetDirty(x, y, false)
-	js.Global().Call("drawCell", x, y, str, fg, bg, int(style.attrs), int(us), int(uc))
-
-	return width
-}
-
-func (t *wScreen) ShowCursor(x, y int) {
-	t.Lock()
-	js.Global().Call("showCursor", x, y)
-	t.Unlock()
-}
-
-func (t *wScreen) SetCursor(cs CursorStyle, cc Color) {
-	if !cc.Valid() {
-		cc = ColorLightGray
-	}
-	t.Lock()
-	js.Global().Call("setCursorStyle", curStyleClasses[cs], fmt.Sprintf("#%06x", cc.Hex()))
-	t.Unlock()
-}
-
-func (t *wScreen) HideCursor() {
-	t.ShowCursor(-1, -1)
-}
-
-func (t *wScreen) Show() {
-	t.Lock()
-	t.resize()
-	t.draw()
-	t.Unlock()
-}
-
-func (t *wScreen) clearScreen() {
-	js.Global().Call("clearScreen", t.style.fg.Hex(), t.style.bg.Hex())
-	t.clear = false
-}
-
-func (t *wScreen) draw() {
-	if t.clear {
-		t.clearScreen()
-	}
-
-	for y := 0; y < t.h; y++ {
-		for x := 0; x < t.w; x++ {
-			width := t.drawCell(x, y)
-			x += width - 1
-		}
-	}
-
-	js.Global().Call("show")
-}
-
-func (t *wScreen) EnableMouse(flags ...MouseFlags) {
-	var f MouseFlags
-	flagsPresent := false
-	for _, flag := range flags {
-		f |= flag
-		flagsPresent = true
-	}
-	if !flagsPresent {
-		f = MouseMotionEvents | MouseDragEvents | MouseButtonEvents
-	}
-
-	t.Lock()
-	t.mouseFlags = f
-	t.enableMouse(f)
-	t.Unlock()
-}
-
-func (t *wScreen) enableMouse(f MouseFlags) {
-	if f&MouseButtonEvents != 0 {
-		js.Global().Set("onMouseClick", js.FuncOf(t.onMouseEvent))
-	} else {
-		js.Global().Set("onMouseClick", js.FuncOf(t.unset))
-	}
-
-	if f&MouseDragEvents != 0 || f&MouseMotionEvents != 0 {
-		js.Global().Set("onMouseMove", js.FuncOf(t.onMouseEvent))
-	} else {
-		js.Global().Set("onMouseMove", js.FuncOf(t.unset))
-	}
-}
-
-func (t *wScreen) DisableMouse() {
-	t.Lock()
-	t.mouseFlags = 0
-	t.enableMouse(0)
-	t.Unlock()
-}
-
-func (t *wScreen) EnablePaste() {
-	t.Lock()
-	t.pasteEnabled = true
-	t.enablePasting(true)
-	t.Unlock()
-}
-
-func (t *wScreen) DisablePaste() {
-	t.Lock()
-	t.pasteEnabled = false
-	t.enablePasting(false)
-	t.Unlock()
-}
-
-func (t *wScreen) enablePasting(on bool) {
-	if on {
-		js.Global().Set("onPaste", js.FuncOf(t.onPaste))
-	} else {
-		js.Global().Set("onPaste", js.FuncOf(t.unset))
-	}
-}
-
-func (t *wScreen) EnableFocus() {
-	t.Lock()
-	js.Global().Set("onFocus", js.FuncOf(t.onFocus))
-	t.Unlock()
-}
-
-func (t *wScreen) DisableFocus() {
-	t.Lock()
-	js.Global().Set("onFocus", js.FuncOf(t.unset))
-	t.Unlock()
-}
-
-func (s *wScreen) GetClipboard() {
-}
-
-func (s *wScreen) SetClipboard(_ []byte) {
-}
-
-func (s *wScreen) HasClipboard() bool {
-	return false
-}
-
-func (t *wScreen) Size() (int, int) {
-	t.Lock()
-	w, h := t.w, t.h
-	t.Unlock()
-	return w, h
-}
-
-// resize does nothing, as asking the web window to resize
-// without a specified width or height will cause no change.
-func (t *wScreen) resize() {}
-
-func (t *wScreen) Colors() int {
-	return 16777216 // 256 ^ 3
-}
-
-func (t *wScreen) clip(x, y int) (int, int) {
-	w, h := t.cells.Size()
-	if x < 0 {
-		x = 0
-	}
-	if y < 0 {
-		y = 0
-	}
-	if x > w-1 {
-		x = w - 1
-	}
-	if y > h-1 {
-		y = h - 1
-	}
-	return x, y
-}
-
-func (t *wScreen) postEvent(ev Event) {
-	select {
-	case t.evch <- ev:
-	case <-t.quit:
-	}
-}
-
-func (t *wScreen) onMouseEvent(this js.Value, args []js.Value) any {
-	mod := ModNone
-	button := ButtonNone
-
-	switch args[2].Int() {
-	case 0:
-		if t.mouseFlags&MouseMotionEvents == 0 {
-			// don't want this event! is a mouse motion event, but user has asked not.
-			return nil
-		}
-		button = ButtonNone
-	case 1:
-		button = Button1
-	case 2:
-		button = Button3 // Note we prefer to treat right as button 2
-	case 3:
-		button = Button2 // And the middle button as button 3
-	}
-
-	if args[3].Bool() { // mod shift
-		mod |= ModShift
-	}
-
-	if args[4].Bool() { // mod alt
-		mod |= ModAlt
-	}
-
-	if args[5].Bool() { // mod ctrl
-		mod |= ModCtrl
-	}
-
-	t.postEvent(NewEventMouse(args[0].Int(), args[1].Int(), button, mod))
-	return nil
-}
-
-func (t *wScreen) onKeyEvent(this js.Value, args []js.Value) any {
-	key := args[0].String()
-
-	// don't accept any modifier keys as their own
-	if key == "Control" || key == "Alt" || key == "Meta" || key == "Shift" {
-		return nil
-	}
-
-	mod := ModNone
-	if args[1].Bool() { // mod shift
-		mod |= ModShift
-	}
-
-	if args[2].Bool() { // mod alt
-		mod |= ModAlt
-	}
-
-	if args[3].Bool() { // mod ctrl
-		mod |= ModCtrl
-	}
-
-	if args[4].Bool() { // mod meta
-		mod |= ModMeta
-	}
-
-	// next try function keys
-	if k, ok := WebKeyNames[key]; ok {
-		t.postEvent(NewEventKey(k, "", mod))
-		return nil
-	}
-
-	// finally try normal, printable chars
-	r, _ := utf8.DecodeRuneInString(key)
-	t.postEvent(NewEventKey(KeyRune, string(r), mod))
-	return nil
-}
-
-func (t *wScreen) onPaste(this js.Value, args []js.Value) any {
-	t.postEvent(NewEventPaste(args[0].Bool()))
-	return nil
-}
-
-func (t *wScreen) onFocus(this js.Value, args []js.Value) any {
-	t.postEvent(NewEventFocus(args[0].Bool()))
-	return nil
-}
-
-// unset is a dummy function for js when we want nothing to
-// happen when javascript calls a function (for example, when
-// mouse input is disabled, when onMouseEvent() is called from
-// js, it redirects here and does nothing).
-func (t *wScreen) unset(this js.Value, args []js.Value) any {
-	return nil
-}
-
-func (t *wScreen) Sync() {
-	t.Lock()
-	t.resize()
-	t.clear = true
-	t.cells.Invalidate()
-	t.draw()
-	t.Unlock()
-}
-
-func (t *wScreen) CharacterSet() string {
+func getCharset() string {
 	return "UTF-8"
 }
 
-func (t *wScreen) RegisterRuneFallback(orig rune, fallback string) {
-	t.Lock()
-	t.fallback[orig] = fallback
-	t.Unlock()
+type browserTty struct {
+	mu      sync.Mutex
+	cond    *sync.Cond
+	started bool
+	drained bool
+	closed  bool
+	input   []byte
+	resizeQ chan<- bool
+
+	writeFunc  js.Value
+	sizeFunc   js.Value
+	closeFuncs []js.Func
 }
 
-func (t *wScreen) UnregisterRuneFallback(orig rune) {
-	t.Lock()
-	delete(t.fallback, orig)
-	t.Unlock()
+func newBrowserTty() *browserTty {
+	t := &browserTty{}
+	t.cond = sync.NewCond(&t.mu)
+	return t
 }
 
-func (t *wScreen) SetSize(w, h int) {
-	if w == t.w && h == t.h {
-		return
-	}
+func (t *browserTty) Start() error {
+	t.mu.Lock()
+	defer t.mu.Unlock()
 
-	t.cells.Invalidate()
-	t.cells.Resize(w, h)
-	js.Global().Call("resize", w, h)
-	t.w, t.h = w, h
-	t.postEvent(NewEventResize(w, h))
-}
-
-func (t *wScreen) Resize(int, int, int, int) {}
-
-// Suspend simply pauses all input and output, and clears the screen.
-// There isn't a "default terminal" to go back to.
-func (t *wScreen) Suspend() error {
-	t.Lock()
-	if !t.running {
-		t.Unlock()
+	if t.started {
 		return nil
 	}
-	t.running = false
-	t.clearScreen()
-	t.enableMouse(0)
-	t.enablePasting(false)
-	js.Global().Set("onKeyEvent", js.FuncOf(t.unset)) // stop key presses
-	return nil
-}
-
-func (t *wScreen) Resume() error {
-	t.Lock()
-
-	if t.running {
-		return errors.New("already engaged")
+	global := js.Global()
+	t.writeFunc = global.Get("tcellWrite")
+	t.sizeFunc = global.Get("tcellWindowSize")
+	if t.writeFunc.Type() != js.TypeFunction || t.sizeFunc.Type() != js.TypeFunction {
+		return errors.New("tcell wasm terminal host is not installed")
 	}
-	t.running = true
 
-	t.enableMouse(t.mouseFlags)
-	t.enablePasting(t.pasteEnabled)
+	onData := js.FuncOf(func(this js.Value, args []js.Value) any {
+		if len(args) == 0 {
+			return nil
+		}
+		if args[0].InstanceOf(global.Get("Uint8Array")) {
+			data := make([]byte, args[0].Get("byteLength").Int())
+			js.CopyBytesToGo(data, args[0])
+			t.enqueue(data)
+		} else {
+			t.enqueue([]byte(args[0].String()))
+		}
+		return nil
+	})
+	onResize := js.FuncOf(func(this js.Value, args []js.Value) any {
+		t.mu.Lock()
+		resizeQ := t.resizeQ
+		t.mu.Unlock()
+		if resizeQ != nil {
+			select {
+			case resizeQ <- true:
+			default:
+			}
+		}
+		return nil
+	})
+	t.closeFuncs = []js.Func{onData, onResize}
+	global.Set("tcellRead", onData)
+	global.Set("tcellResize", onResize)
 
-	js.Global().Set("onKeyEvent", js.FuncOf(t.onKeyEvent))
-
-	t.Unlock()
+	t.started = true
+	t.drained = false
+	t.closed = false
 	return nil
 }
 
-func (t *wScreen) Beep() error {
-	js.Global().Call("beep")
+func (t *browserTty) Stop() error {
+	t.mu.Lock()
+	t.started = false
+	t.drained = false
+	funcs := t.closeFuncs
+	t.closeFuncs = nil
+	t.cond.Broadcast()
+	t.mu.Unlock()
+
+	js.Global().Set("tcellRead", js.Undefined())
+	js.Global().Set("tcellResize", js.Undefined())
+	for _, fn := range funcs {
+		fn.Release()
+	}
 	return nil
 }
 
-func (t *wScreen) Tty() (Tty, bool) {
-	return nil, false
+func (t *browserTty) Drain() error {
+	t.mu.Lock()
+	t.input = nil
+	t.drained = true
+	t.cond.Broadcast()
+	t.mu.Unlock()
+	return nil
 }
 
-func (t *wScreen) GetCells() *CellBuffer {
-	return &t.cells
+func (t *browserTty) NotifyResize(resizeQ chan<- bool) {
+	t.mu.Lock()
+	t.resizeQ = resizeQ
+	t.mu.Unlock()
 }
 
-func (t *wScreen) EventQ() chan Event {
-	return t.evch
+func (t *browserTty) WindowSize() (tty.WindowSize, error) {
+	var ws tty.WindowSize
+	t.mu.Lock()
+	sizeFunc := t.sizeFunc
+	t.mu.Unlock()
+	if sizeFunc.Type() != js.TypeFunction {
+		ws.Width = 80
+		ws.Height = 24
+		return ws, nil
+	}
+	size := sizeFunc.Invoke()
+	ws.Width = size.Get("cols").Int()
+	ws.Height = size.Get("rows").Int()
+	ws.PixelWidth = size.Get("pixelWidth").Int()
+	ws.PixelHeight = size.Get("pixelHeight").Int()
+	if ws.Width == 0 {
+		ws.Width = 80
+	}
+	if ws.Height == 0 {
+		ws.Height = 24
+	}
+	return ws, nil
 }
 
-func (t *wScreen) StopQ() <-chan struct{} {
-	return t.quit
+func (t *browserTty) Read(b []byte) (int, error) {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+
+	for len(t.input) == 0 && t.started && !t.drained && !t.closed {
+		t.cond.Wait()
+	}
+	if t.closed {
+		return 0, io.EOF
+	}
+	if (!t.started || t.drained) && len(t.input) == 0 {
+		return 0, io.EOF
+	}
+	n := copy(b, t.input)
+	t.input = t.input[n:]
+	return n, nil
 }
 
-func (t *wScreen) SetTitle(title string) {
-	js.Global().Call("setTitle", title)
+func (t *browserTty) Write(b []byte) (int, error) {
+	t.mu.Lock()
+	writeFunc := t.writeFunc
+	started := t.started
+	t.mu.Unlock()
+	if !started || writeFunc.Type() != js.TypeFunction {
+		return 0, io.ErrClosedPipe
+	}
+
+	data := js.Global().Get("Uint8Array").New(len(b))
+	js.CopyBytesToJS(data, b)
+	writeFunc.Invoke(data)
+	return len(b), nil
 }
 
-func (*wScreen) ShowNotification(title string, body string) {}
+func (t *browserTty) Close() error {
+	t.mu.Lock()
+	if t.closed {
+		t.mu.Unlock()
+		return nil
+	}
+	t.closed = true
+	t.started = false
+	t.drained = false
+	t.cond.Broadcast()
+	t.mu.Unlock()
 
-// WebKeyNames maps string names reported from HTML
-// (KeyboardEvent.key) to tcell accepted keys.
-var WebKeyNames = map[string]Key{
-	"Enter":      KeyEnter,
-	"Backspace":  KeyBackspace,
-	"Tab":        KeyTab,
-	"Backtab":    KeyBacktab,
-	"Escape":     KeyEsc,
-	"Backspace2": KeyBackspace2,
-	"Delete":     KeyDelete,
-	"Insert":     KeyInsert,
-	"ArrowUp":    KeyUp,
-	"ArrowDown":  KeyDown,
-	"ArrowLeft":  KeyLeft,
-	"ArrowRight": KeyRight,
-	"Home":       KeyHome,
-	"End":        KeyEnd,
-	"UpLeft":     KeyUpLeft,    // not supported by HTML
-	"UpRight":    KeyUpRight,   // not supported by HTML
-	"DownLeft":   KeyDownLeft,  // not supported by HTML
-	"DownRight":  KeyDownRight, // not supported by HTML
-	"Center":     KeyCenter,
-	"PgDn":       KeyPgDn,
-	"PgUp":       KeyPgUp,
-	"Clear":      KeyClear,
-	"Exit":       KeyExit,
-	"Cancel":     KeyCancel,
-	"Pause":      KeyPause,
-	"Print":      KeyPrint,
-	"F1":         KeyF1,
-	"F2":         KeyF2,
-	"F3":         KeyF3,
-	"F4":         KeyF4,
-	"F5":         KeyF5,
-	"F6":         KeyF6,
-	"F7":         KeyF7,
-	"F8":         KeyF8,
-	"F9":         KeyF9,
-	"F10":        KeyF10,
-	"F11":        KeyF11,
-	"F12":        KeyF12,
-	"F13":        KeyF13,
-	"F14":        KeyF14,
-	"F15":        KeyF15,
-	"F16":        KeyF16,
-	"F17":        KeyF17,
-	"F18":        KeyF18,
-	"F19":        KeyF19,
-	"F20":        KeyF20,
-	"F21":        KeyF21,
-	"F22":        KeyF22,
-	"F23":        KeyF23,
-	"F24":        KeyF24,
-	"F25":        KeyF25,
-	"F26":        KeyF26,
-	"F27":        KeyF27,
-	"F28":        KeyF28,
-	"F29":        KeyF29,
-	"F30":        KeyF30,
-	"F31":        KeyF31,
-	"F32":        KeyF32,
-	"F33":        KeyF33,
-	"F34":        KeyF34,
-	"F35":        KeyF35,
-	"F36":        KeyF36,
-	"F37":        KeyF37,
-	"F38":        KeyF38,
-	"F39":        KeyF39,
-	"F40":        KeyF40,
-	"F41":        KeyF41,
-	"F42":        KeyF42,
-	"F43":        KeyF43,
-	"F44":        KeyF44,
-	"F45":        KeyF45,
-	"F46":        KeyF46,
-	"F47":        KeyF47,
-	"F48":        KeyF48,
-	"F49":        KeyF49,
-	"F50":        KeyF50,
-	"F51":        KeyF51,
-	"F52":        KeyF52,
-	"F53":        KeyF53,
-	"F54":        KeyF54,
-	"F55":        KeyF55,
-	"F56":        KeyF56,
-	"F57":        KeyF57,
-	"F58":        KeyF58,
-	"F59":        KeyF59,
-	"F60":        KeyF60,
-	"F61":        KeyF61,
-	"F62":        KeyF62,
-	"F63":        KeyF63,
-	"F64":        KeyF64,
+	return t.Stop()
 }
 
-var curStyleClasses = map[CursorStyle]string{
-	CursorStyleDefault:           "cursor-blinking-block",
-	CursorStyleBlinkingBlock:     "cursor-blinking-block",
-	CursorStyleSteadyBlock:       "cursor-steady-block",
-	CursorStyleBlinkingUnderline: "cursor-blinking-underline",
-	CursorStyleSteadyUnderline:   "cursor-steady-underline",
-	CursorStyleBlinkingBar:       "cursor-blinking-bar",
-	CursorStyleSteadyBar:         "cursor-steady-bar",
+func (t *browserTty) enqueue(data []byte) {
+	t.mu.Lock()
+	if t.started && !t.closed {
+		t.input = append(t.input, data...)
+		t.cond.Broadcast()
+	}
+	t.mu.Unlock()
 }
-
-func (*wScreen) Terminal() (string, string) { return "Tcell-WebASM", "" }
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 0b303b80c..6a21d6d97 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -48,7 +48,7 @@ github.com/fatih/color
 # github.com/gdamore/encoding v1.0.1
 ## explicit; go 1.9
 github.com/gdamore/encoding
-# github.com/gdamore/tcell/v3 v3.3.0
+# github.com/gdamore/tcell/v3 v3.4.0
 ## explicit; go 1.25.0
 github.com/gdamore/tcell/v3
 github.com/gdamore/tcell/v3/color
@@ -184,10 +184,10 @@ golang.org/x/sync/errgroup
 golang.org/x/sys/plan9
 golang.org/x/sys/unix
 golang.org/x/sys/windows
-# golang.org/x/term v0.42.0
+# golang.org/x/term v0.43.0
 ## explicit; go 1.25.0
 golang.org/x/term
-# golang.org/x/text v0.36.0
+# golang.org/x/text v0.37.0
 ## explicit; go 1.25.0
 golang.org/x/text/cases
 golang.org/x/text/encoding