diff --git a/go.mod b/go.mod index 9d36dacdfc2..bc59557dbe1 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,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.4.1 + github.com/gdamore/tcell/v3 v3.4.2 github.com/go-errors/errors v1.5.1 github.com/gookit/color v1.6.1 github.com/integrii/flaggy v1.8.0 @@ -68,7 +68,7 @@ require ( go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/mod v0.38.0 // indirect golang.org/x/term v0.45.0 // indirect - golang.org/x/text v0.40.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/tools v0.48.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect diff --git a/go.sum b/go.sum index 6e95e0053c8..8b12ec9b3d0 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,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.4.1 h1:22227t1EUwqxTlmCX9vw0RUE2IEPGw6oYcNan+bPe4w= -github.com/gdamore/tcell/v3 v3.4.1/go.mod h1:YWwuxZNi14VGQC5g2VGNEDRXpBraTwvVjMovRH6G6hw= +github.com/gdamore/tcell/v3 v3.4.2 h1:gGW+6z2Bz5Wl2mNwFlm9+eRmg2JQrWcKjSkL1LRfpNU= +github.com/gdamore/tcell/v3 v3.4.2/go.mod h1:Oe5U3S3jm3NzypswDNUhe+LUnF5CoFq2b4sepD++QHo= 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= @@ -174,8 +174,8 @@ 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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= 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/cell.go b/vendor/github.com/gdamore/tcell/v3/cell.go index b3be03b1356..73756b37dcb 100644 --- a/vendor/github.com/gdamore/tcell/v3/cell.go +++ b/vendor/github.com/gdamore/tcell/v3/cell.go @@ -14,6 +14,8 @@ package tcell +import "unicode/utf8" + type cell struct { currStr string lastStr string @@ -76,6 +78,10 @@ func (cb *CellBuffer) put(x int, y int, str string, style Style) (string, int) { // Identical re-Put (a full-screen redraw): the grapheme split is // unchanged, so reuse the measured width instead of segmenting. cl, width, str = str, c.width, "" + } else if len(str) > 0 && str[0] >= ' ' && str[0] <= '~' && (len(str) == 1 || str[1] < utf8.RuneSelf) { + // Printable ASCII followed by ASCII cannot be part of a larger + // grapheme cluster, so avoid constructing a grapheme iterator. + cl, width, str = str[:1], 1, str[1:] } else { g := textWidthOptions.StringGraphemes(str) for width == 0 && g.Next() { diff --git a/vendor/github.com/gdamore/tcell/v3/input.go b/vendor/github.com/gdamore/tcell/v3/input.go index 74e234d1ec1..0c279f99827 100644 --- a/vendor/github.com/gdamore/tcell/v3/input.go +++ b/vendor/github.com/gdamore/tcell/v3/input.go @@ -63,10 +63,22 @@ const ( // before they can grow without bound while waiting for a string terminator. const defaultControlStringLimit = 64 * 1024 +const ( + // loneEscapeTimeout keeps bare Escape responsive when using legacy + // keyboard reporting, where ESC can also prefix an Alt-modified key. + loneEscapeTimeout = 200 * time.Millisecond + + // escapeSequenceTimeout bounds incomplete escape sequences. Once a + // sequence introducer has arrived, it is no longer ambiguous with a lone + // Escape and can tolerate a substantially longer inter-byte delay. + escapeSequenceTimeout = time.Second +) + func newInputParser(eq chan<- Event) *inputParser { return &inputParser{ evch: eq, buf: make([]rune, 0, 128), + legacy: true, controlStringMax: defaultControlStringLimit, } } @@ -91,6 +103,7 @@ type inputParser struct { 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 + legacy bool // keyboard protocol has ambiguous ESC prefixes controlStringMax int // maximum inbound OSC/XDA payload size; 0 means unlimited discardString bool // drop the rest of an over-limit OSC/XDA sequence } @@ -129,6 +142,35 @@ func (ip *inputParser) Waiting() bool { return ip.state != istInit } +// waitDuration reports how long to wait for the next byte before resetting an +// incomplete escape sequence. A bare ESC is only ambiguous with legacy +// keyboard reporting; other protocols can use the longer sequence deadline. +func (ip *inputParser) waitDuration() time.Duration { + if ip.state == istInit { + return 0 + } + if ip.state == istEsc && ip.legacy { + return loneEscapeTimeout + } + return escapeSequenceTimeout +} + +func (ip *inputParser) WaitDuration() time.Duration { + ip.l.Lock() + defer ip.l.Unlock() + return ip.waitDuration() +} + +func (ip *inputParser) SetKeyboardProtocol(protocol KeyProtocol) { + ip.l.Lock() + ip.legacy = protocol == LegacyKeyboard + nested := ip.nested + ip.l.Unlock() + if nested != nil { + nested.SetKeyboardProtocol(protocol) + } +} + // 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. @@ -488,25 +530,46 @@ var winKeys = map[int]Key{ 0x87: KeyF24, // vkF24 } +type ss3Key struct { + key Key + str string +} + // keys by their SS3 - used in application mode usually (legacy VT-style) -var ss3Keys = map[rune]Key{ - 'A': KeyUp, - 'B': KeyDown, - 'C': KeyRight, - 'D': KeyLeft, - 'E': KeyClear, - 'F': KeyEnd, - 'H': KeyHome, - 'P': KeyF1, - 'Q': KeyF2, - 'R': KeyF3, - 'S': KeyF4, - 't': KeyF5, - 'u': KeyF6, - 'v': KeyF7, - 'l': KeyF8, - 'w': KeyF9, - 'x': KeyF10, +var ss3Keys = map[rune]ss3Key{ + 'A': {key: KeyUp}, + 'B': {key: KeyDown}, + 'C': {key: KeyRight}, + 'D': {key: KeyLeft}, + 'E': {key: KeyClear}, + 'F': {key: KeyEnd}, + 'H': {key: KeyHome}, + 'P': {key: KeyF1}, + 'Q': {key: KeyF2}, + 'R': {key: KeyF3}, + 'S': {key: KeyF4}, + + // DEC application-keypad sequences. The VT100 terminfo entry calls some + // of these F5-F10, but that is a terminfo naming artifact: a VT100 has + // only PF1-PF4. Decode them by their PC keypad navigation meanings. + 'p': {key: KeyInsert}, + 'q': {key: KeyEnd}, + 'r': {key: KeyDown}, + 's': {key: KeyPgDn}, + 't': {key: KeyLeft}, + 'u': {key: KeyClear}, + 'v': {key: KeyRight}, + 'w': {key: KeyHome}, + 'x': {key: KeyUp}, + 'y': {key: KeyPgUp}, + 'M': {key: KeyEnter}, + 'n': {key: KeyDelete}, + 'j': {key: KeyRune, str: "*"}, + 'k': {key: KeyRune, str: "+"}, + 'l': {key: KeyRune, str: ","}, + 'm': {key: KeyRune, str: "-"}, + 'o': {key: KeyRune, str: "/"}, + 'X': {key: KeyRune, str: "="}, } // linux terminal uses these non ECMA keys prefixed by CSI-[ @@ -669,16 +732,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.postKey(k, "", ModNone) + ip.postKey(k.key, k.str, 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.postKey(k, "", calcModifier(m)) + ip.postKey(k.key, k.str, calcModifier(m)) } } else if m, err := strconv.Atoi(parts[0]); err == nil { - ip.postKey(k, "", calcModifier(m)) + ip.postKey(k.key, k.str, calcModifier(m)) } } } @@ -757,7 +820,7 @@ func (ip *inputParser) scan() { } } - if ip.state != istInit && time.Since(ip.keyTime) > time.Millisecond*50 { + if timeout := ip.waitDuration(); timeout > 0 && time.Since(ip.keyTime) > timeout { if ip.state == istEsc { ip.postKey(KeyEscape, "", ModNone) } else if ec := ip.escChar; ec != 0 { @@ -1056,6 +1119,7 @@ func (ip *inputParser) handleWinKey(P []int) { rows: ip.rows, cols: ip.cols, advanced: ip.advanced, + legacy: ip.legacy, pixelMouse: ip.pixelMouse, controlStringMax: ip.controlStringMax, } @@ -1425,7 +1489,7 @@ func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte) // this might have been an SS3 style key with modifiers applied if k, ok := ss3Keys[mode]; ok && P0 == 1 && len(P) > 1 { - ip.postKeyEx(k, "", calcModifier(P[1]), pressed, 0, repeat) + ip.postKeyEx(k.key, k.str, 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/tscreen.go b/vendor/github.com/gdamore/tcell/v3/tscreen.go index d50101903ac..bbd31e31699 100644 --- a/vendor/github.com/gdamore/tcell/v3/tscreen.go +++ b/vendor/github.com/gdamore/tcell/v3/tscreen.go @@ -286,6 +286,11 @@ type tScreen struct { advancedKeys bool controlStringLimit int input *inputParser + compat struct { + mouseUnsupported bool + focusUnsupported bool + clipboardReadUnsupported bool + } sync.Mutex } @@ -780,6 +785,10 @@ func (t *tScreen) emitAttrs(attrs AttrMask) { // The assumption is that sgr0 was already printed ahead of this. func (t *tScreen) emitUnderline(us UnderlineStyle, uc Color) { if us != UnderlineStyleNone { + if t.legacy { + t.Print(underline) + return + } // NB: under color should have been reset by sgr0 if uc.IsRGB() { r, g, b := uc.RGB() @@ -1100,6 +1109,9 @@ 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.compat.mouseUnsupported { + return + } if t.mouseDisabled { f = 0 } @@ -1203,10 +1215,16 @@ func (t *tScreen) DisableFocus() { } func (t *tScreen) enableFocusReporting() { + if t.compat.focusUnsupported { + return + } t.Print(vt.PmFocusReports.Enable()) } func (t *tScreen) disableFocusReporting() { + if t.compat.focusUnsupported { + return + } t.Print(vt.PmFocusReports.Disable()) } @@ -1336,13 +1354,18 @@ func (t *tScreen) mainLoop(stopQ chan struct{}) { case chunk := <-t.keyQ: buf.Write(chunk) t.scanInput(buf) - if t.input.Waiting() { - ta = time.After(time.Millisecond * 100) + if timeout := t.input.WaitDuration(); timeout > 0 { + ta = time.After(timeout) } else { ta = nil } case <-ta: t.input.Scan() + if timeout := t.input.WaitDuration(); timeout > 0 { + ta = time.After(timeout) + } else { + ta = nil + } } } } @@ -1373,7 +1396,11 @@ func (t *tScreen) inputLoop(stopQ chan struct{}) { return } if n > 0 { - t.keyQ <- chunk[:n] + select { + case t.keyQ <- chunk[:n]: + case <-t.quit: + return + } } } } @@ -1449,7 +1476,31 @@ func (t *tScreen) Tty() (Tty, bool) { return t.tty, true } -func (t *tScreen) applyKnownTerminalProfile(goos, termProgram string) bool { +func isSTTerminal(term string) bool { + return term == "st" || strings.HasPrefix(term, "st-") +} + +func (t *tScreen) applyKnownTerminalProfile(goos, term, termProgram string) bool { + if isSTTerminal(term) { + // st implements a small subset of xterm extensions. In particular, + // it has neither an advanced keyboard protocol nor SGR mouse or focus + // reporting. It also reports unsupported CSI and OSC sequences to + // stderr, so avoid probing or using extensions it does not implement. + t.legacy = true + t.compat.mouseUnsupported = true + t.compat.focusUnsupported = true + t.enterUrl = "" + t.exitUrl = "" + t.setWinSize = "" + t.saveTitle = "" + t.restoreTitle = "" + t.setTitle = "\x1b]2;%s\x1b\\" + t.notifyDesktop = "" + t.compat.clipboardReadUnsupported = true + t.termName = "st" + return true + } + switch termProgram { case "Apple_Terminal": // macOS Terminal.app cannot handle the startup queries, but it does @@ -1525,7 +1576,7 @@ func (t *tScreen) engageLocked() error { // 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 !t.applyKnownTerminalProfile(runtime.GOOS, os.Getenv("TERM_PROGRAM")) && t.negotiate { + if !t.applyKnownTerminalProfile(runtime.GOOS, t.term, os.Getenv("TERM_PROGRAM")) && t.negotiate { if useVTWindowSizeQuery(runtime.GOOS) { t.Print(requestWindowSize) } @@ -1551,6 +1602,7 @@ func (t *tScreen) engageLocked() error { } t.processInitQ() t.applyKeyboardProtocolOverride() + t.input.SetKeyboardProtocol(t.keyboardProtocol()) 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 @@ -1589,7 +1641,7 @@ func (t *tScreen) engageLocked() error { if t.title != "" && t.setTitle != "" { t.Printf(t.setTitle, t.title) } - if t.negotiate && useVTWindowSizeQuery(runtime.GOOS) { + if t.negotiate && !t.legacy && useVTWindowSizeQuery(runtime.GOOS) { t.Print(requestWindowSize) } @@ -1744,7 +1796,7 @@ func (t *tScreen) GetClipboard() { t.Unlock() return } - if t.setClipboard != "" { + if !t.compat.clipboardReadUnsupported && t.setClipboard != "" { t.Printf(t.setClipboard, "?") } t.Unlock() @@ -1773,6 +1825,11 @@ func (t *tScreen) Terminal() (string, string) { func (t *tScreen) KeyboardProtocol() KeyProtocol { t.Lock() defer t.Unlock() + return t.keyboardProtocol() +} + +// keyboardProtocol reports the selected keyboard protocol while t is locked. +func (t *tScreen) keyboardProtocol() KeyProtocol { if t.haveWin32Kbd { return Win32Keyboard } diff --git a/vendor/modules.txt b/vendor/modules.txt index c56b7971f40..5ee921660c9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -45,7 +45,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.4.1 +# github.com/gdamore/tcell/v3 v3.4.2 ## explicit; go 1.25.0 github.com/gdamore/tcell/v3 github.com/gdamore/tcell/v3/color @@ -185,7 +185,7 @@ golang.org/x/sys/windows # golang.org/x/term v0.45.0 ## explicit; go 1.25.0 golang.org/x/term -# golang.org/x/text v0.40.0 +# golang.org/x/text v0.41.0 ## explicit; go 1.25.0 golang.org/x/text/cases golang.org/x/text/encoding