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