This commit is contained in:
Stefan Haller 2026-09-11 19:16:11 +02:00 committed by GitHub
commit 4ecb10394e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 112 additions and 26 deletions

2
go.mod
View file

@ -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.2
github.com/gdamore/tcell/v3 v3.5.0
github.com/go-errors/errors v1.5.1
github.com/gookit/color v1.6.1
github.com/integrii/flaggy v1.8.0

4
go.sum
View file

@ -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.2 h1:gGW+6z2Bz5Wl2mNwFlm9+eRmg2JQrWcKjSkL1LRfpNU=
github.com/gdamore/tcell/v3 v3.4.2/go.mod h1:Oe5U3S3jm3NzypswDNUhe+LUnF5CoFq2b4sepD++QHo=
github.com/gdamore/tcell/v3 v3.5.0 h1:SCp9czLv2K2aPORgD6+4fjV0xNAzKIkiezCkp6bHLe4=
github.com/gdamore/tcell/v3 v3.5.0/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=

View file

@ -252,3 +252,45 @@ func (cb *CellBuffer) Fill(r rune, style Style) {
c.width = 1
}
}
// FillArea fills a rectangular region of the cell buffer with the specified
// character and style. The region starts at column x, row y and extends w
// columns to the right and h rows down; any part of it lying outside the
// buffer is simply skipped, so callers do not need to clip coordinates
// themselves. A zero or negative width or height fills nothing. As with
// Fill, this doesn't support combining characters or wide runes, and a
// ColorNone foreground or background leaves that color unchanged.
func (cb *CellBuffer) FillArea(x, y, w, h int, r rune, style Style) {
if w <= 0 || h <= 0 {
return
}
x0 := max(x, 0)
y0 := max(y, 0)
// Clip the far edge to the buffer without ever evaluating x+w or y+h when
// they would overflow a signed int. Because w and h are positive here,
// x < cb.w-w is equivalent to x+w < cb.w but cannot overflow, and it is
// only true when x+w is small enough to compute safely.
x1 := cb.w
if x < cb.w-w {
x1 = x + w
}
y1 := cb.h
if y < cb.h-h {
y1 = y + h
}
for row := y0; row < y1; row++ {
for col := x0; col < x1; col++ {
c := &cb.cells[(row*cb.w)+col]
c.currStr = string(r)
cs := style
if cs.fg == ColorNone {
cs.fg = c.currStyle.fg
}
if cs.bg == ColorNone {
cs.bg = c.currStyle.bg
}
c.currStyle = cs
c.width = 1
}
}
}

View file

@ -64,9 +64,10 @@ const (
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
// loneEscapeTimeout keeps bare Escape responsive. A lone ESC byte is
// always ambiguous, because it can also prefix an Alt-modified key or a
// longer sequence, so it cannot be resolved until this expires.
loneEscapeTimeout = 50 * time.Millisecond
// escapeSequenceTimeout bounds incomplete escape sequences. Once a
// sequence introducer has arrived, it is no longer ambiguous with a lone
@ -129,27 +130,15 @@ func asciiByteFromInt(n int) (byte, bool) {
return byte(n), true
}
// Waiting returns true if the processor is waiting for
// some more input (i.e. we are not in in the initial state.)
// This can occur when we have ambiguous escape sequences, such
// as the lone escape. If this is typed, we expect at least a minimal
// inter-key delay before the next stroke occurs, and the caller
// should check for waiting, and call Scan() or ScanUTF8() to
// finish the processing. (Typically after a delay of around 100ms.)
func (ip *inputParser) Waiting() bool {
ip.l.Lock()
defer ip.l.Unlock()
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.
// incomplete escape sequence. A bare ESC is ambiguous under every keyboard
// protocol, so it gets the short deadline; only once an introducer has arrived
// is the longer sequence deadline used.
func (ip *inputParser) waitDuration() time.Duration {
if ip.state == istInit {
return 0
}
if ip.state == istEsc && ip.legacy {
if ip.state == istEsc {
return loneEscapeTimeout
}
return escapeSequenceTimeout
@ -867,6 +856,9 @@ func (ip *inputParser) handleXda(str string) {
}
func calcModifier(n int) ModMask {
if n < 1 {
return ModNone
}
n--
m := ModNone
if n&1 != 0 {
@ -987,6 +979,26 @@ func kittyModifierKey(code int) ModMask {
}
}
// kittyKeyText extracts the associated text (kitty mode 16) from a csi-u
// event's params: the third ;-field, codepoints :separated. Empty when
// the event carries no text (control keys, specials, terminals without
// mode 16), so callers fall back to the base key.
func kittyKeyText(params string) string {
fields := strings.Split(params, ";")
if len(fields) < 3 || fields[2] == "" {
return ""
}
var b strings.Builder
// Reject C0 control chars, DEL, and C1 control chars: they must never
// surface as key text.
for cp := range strings.SplitSeq(fields[2], ":") {
if n, err := strconv.ParseInt(cp, 10, 32); err == nil && n >= 0x20 && (n < 0x7f || n > 0x9f) && utf8.ValidRune(rune(n)) {
b.WriteRune(rune(n))
}
}
return b.String()
}
func (ip *inputParser) handleMouse(mode rune, params []int) {
// XTerm mouse events only report at most one button at a time,
@ -1421,6 +1433,12 @@ func (ip *inputParser) handleCsi(mode rune, params []byte, intermediate []byte)
if mod1 := kittyModifierKey(P0); mod1 != ModNone {
mod |= mod1
}
// kitty mode 16: text is the layout-correct output, sent with its
// modifiers - keep both. No text falls through to the base key.
if text := kittyKeyText(pstr); text != "" {
ip.postKeyEx(KeyRune, text, mod, pressed, physical, repeat)
return
}
if key != KeyRune {
ip.postKeyEx(key, "", mod, pressed, physical, repeat)
} else if chr != 0 {

View file

@ -39,6 +39,14 @@ type Screen interface {
// is called (or Sync).
Fill(rune, Style)
// FillArea fills a rectangular region of the screen with the given
// character and style. The region starts at column x, row y and
// extends width columns to the right and height rows down. Any part
// of the region outside the screen is ignored, so it's safe to pass
// coordinates that overflow the screen. Like Fill, the change is not
// visible until Show (or Sync) is called.
FillArea(x int, y int, width int, height int, r rune, style Style)
// Put writes the first grapheme of the given string with th
// given style at the given coordinates. (Only the first grapheme
// occupying either one or two cells is stored.) It returns the
@ -426,6 +434,13 @@ func (b *baseScreen) Fill(r rune, style Style) {
b.Unlock()
}
func (b *baseScreen) FillArea(x, y, width, height int, r rune, style Style) {
cb := b.GetCells()
b.Lock()
cb.FillArea(x, y, width, height, r, style)
b.Unlock()
}
func (b *baseScreen) SetContent(x, y int, mainc rune, combc []rune, style Style) {
b.Put(x, y, string(append([]rune{mainc}, combc...)), style)
}

View file

@ -179,7 +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
enableKittyKbdAdv = "\x1b[=31u" // disambiguation, events, alternate keys, all keys, text
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
@ -1374,13 +1374,24 @@ func (t *tScreen) inputLoop(stopQ chan struct{}) {
defer t.wg.Done()
for {
readDone := make(chan bool)
chunk := make([]byte, 128)
var n int
var e error
select {
case <-stopQ:
return
default:
go func() {
n, e = t.tty.Read(chunk)
close(readDone)
}()
select {
case <-stopQ:
return
case <-readDone:
}
}
chunk := make([]byte, 128)
n, e := t.tty.Read(chunk)
switch e {
case nil:
default:

2
vendor/modules.txt vendored
View file

@ -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.2
# github.com/gdamore/tcell/v3 v3.5.0
## explicit; go 1.25.0
github.com/gdamore/tcell/v3
github.com/gdamore/tcell/v3/color