okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
7bfbaab
main
1package uv 2 3import ( 4"bytes" 5"context" 6"errors" 7"fmt" 8"io" 9"strconv" 10"strings" 11"sync" 12"time" 13"unicode" 14"unicode/utf16" 15"unicode/utf8" 16 17"github.com/charmbracelet/x/ansi" 18"github.com/muesli/cancelreader" 19"github.com/rivo/uniseg" 20) 21 22// ErrReaderNotStarted is returned when the reader has not been started yet. 23var ErrReaderNotStarted = fmt .Errorf ("reader not started" ) 24 25// DefaultEscTimeout is the default timeout at which the [TerminalReader] will 26// process ESC sequences. It is set to 50 milliseconds. 27const DefaultEscTimeout = 50 * time .Millisecond 28 29// An OSC 5522 clipboard reply chunk spans several reads, and over a network 30// the rest can arrive well after [DefaultEscTimeout]. Flushing it then would 31// type its payload into the program, so a buffer that starts one waits this 32// long after its last byte, up to maxPendingClipboardOsc bytes. Other 33// sequences keep the upstream timeout. 34var pendingClipboardOscTimeout = 5 * time .Second 35 36const maxPendingClipboardOsc = 1 << 20 37 38// TerminalReader represents an input event loop that reads input events from 39// a reader and parses them into human-readable events. It supports 40// reading escape sequences, mouse events, and bracketed paste mode. 41type TerminalReader struct { 42EventDecoder 43 44// MouseMode determines whether mouse events are enabled or not. This is a 45// platform-specific feature and is only available on Windows. When this is 46// true, the reader will be initialized to read mouse events using the 47// Windows Console API. 48MouseMode * MouseMode 49 50// EscTimeout is the escape character timeout duration. Most escape 51// sequences start with an escape character [ansi.ESC] and are followed by 52// one or more characters. If the next character is not received within 53// this timeout, the reader will assume that the escape sequence is 54// complete and will process the received characters as a complete escape 55// sequence. 56// 57// By default, this is set to [DefaultEscTimeout] (50 milliseconds). 58EscTimeout time.Duration 59 60r io.Reader 61table map [string ]Key // table is a lookup table for key sequences. 62 63term string // term is the terminal name $TERM. 64 65// paste is the bracketed paste mode buffer. 66// When nil, bracketed paste mode is disabled. 67paste []byte 68 69lookup bool // lookup indicates whether to use the lookup table for key sequences. 70 71// vtInput indicates whether we're using Windows Console API VT input mode. 72//nolint:unused,nolintlint 73vtInput bool 74 75eventScanner * eventScanner 76 77// We use these buffers to decode UTF-16 sequences and graphemes from the 78// Windows Console API and Win32-Input-Mode events. 79utf16Half [2 ]bool // 0 key up, 1 key down 80utf16Buf [2 ][2 ]rune // 0 key up, 1 key down 81graphemeBuf [2 ][]rune // 0 key up, 1 key down 82//nolint:unused,nolintlint 83lastMouseBtns uint32 // the last mouse button state for the previous event 84//nolint:unused,nolintlint 85lastWinsizeX ,lastWinsizeY int16 // the last window size for the previous event to prevent multiple size events from firing 86 87logger Logger // The logger to use for debugging. 88} 89 90// NewTerminalReader returns a new input event reader. The reader streams input 91// events from the terminal and parses escape sequences into human-readable 92// events. It supports reading Terminfo databases. 93// 94// Use [TerminalReader.UseTerminfo] to use Terminfo defined key sequences. 95// Use [TerminalReader.Legacy] to control legacy key encoding behavior. 96// 97// Example: 98// 99// ```go 100// var cr cancelreader.CancelReader 101// var evc chan Event 102// sc := NewTerminalReader(cr, os.Getenv("TERM")) 103// go sc.StreamEvents(ctx, evc) 104// ``` 105func NewTerminalReader (r io.Reader ,termType string )* TerminalReader { 106d := & TerminalReader { 107EscTimeout :DefaultEscTimeout , 108r :r , 109term :termType , 110lookup :true ,// Use lookup table by default. 111 } 112d .r = r 113if d .table == nil { 114d .table = buildKeysTable (d .Legacy ,d .term ,d .UseTerminfo ) 115 } 116evs := newEventScanner () 117evs .EventDecoder = d .EventDecoder 118evs .table = d .table 119evs .lookup = d .lookup 120evs .setLogger (d .logger ) 121d .eventScanner = evs 122return d 123} 124 125// readBufSize is the size of the read buffer used to read input events at a time. 126const readBufSize = 4096 127 128// sendBytes reads data from the reader and sends it to the provided channel. 129// It stops when an error occurs or when the context is closed. 130func (d * TerminalReader )sendBytes (ctx context.Context ,readc chan []byte )error { 131for { 132var readBuf [readBufSize ]byte 133n ,err := d .r .Read (readBuf [:]) 134if err != nil { 135return err //nolint:wrapcheck 136 } 137 138select { 139case <- ctx .Done (): 140return nil 141case readc <- readBuf [:n ]: 142 } 143 } 144} 145 146// StreamEvents sends events to the provided channel. It stops when the context 147// is closed or when an error occurs. 148func (d * TerminalReader )StreamEvents (ctx context.Context ,eventc chan <- Event )error { 149var buf bytes.Buffer 150errc := make (chan error ,1 ) 151readc := make (chan []byte ) 152timeout := time .NewTimer (d .EscTimeout ) 153ttimeout := time .Now ().Add (d .EscTimeout ) 154var lastRead time.Time 155 156var wg sync.WaitGroup 157wg .Add (1 ) 158 159go func () { 160defer wg .Done () 161if err := d .streamData (ctx ,readc );err != nil { 162if errors .Is (err ,io .EOF )|| errors .Is (err ,cancelreader .ErrCanceled ) { 163errc <- nil 164return 165 } 166errc <- err 167return 168 } 169 }() 170 171for { 172select { 173case <- ctx .Done (): 174d .sendEvents (eventc ,buf .Bytes (),true ) 175wg .Wait () 176return nil 177case err := <- errc : 178d .sendEvents (eventc ,buf .Bytes (),true ) 179wg .Wait () 180return err // return the first error encountered 181case <- timeout .C : 182d .logf ("timeout reached" ) 183 184// Timeout reached process the buffer including any incomplete sequences. 185var n int // n is the number of bytes processed 186timedout := time .Now ().After (ttimeout ) 187if timedout && awaitingClipboardReply (buf .Bytes (),lastRead ) { 188d .logf ("waiting for the rest of a clipboard reply" ) 189timeout .Reset (d .EscTimeout ) 190continue 191 } 192if buf .Len ()> 0 && timedout { 193d .logf ("timeout expired, processing buffer" ) 194n = d .sendEvents (eventc ,buf .Bytes (),true ) 195 } 196 197if n > 0 { 198buf .Next (n ) 199 } 200 201if buf .Len ()> 0 { 202if ! timeout .Stop () { 203// drain the channel if it was already running 204select { 205case <- timeout .C : 206default : 207 } 208 } 209 210d .logf ("resetting timeout for remaining buffer" ) 211timeout .Reset (d .EscTimeout ) 212 } 213 214case read := <- readc : 215d .logf ("input: %q" ,read ) 216buf .Write (read ) 217lastRead = time .Now () 218ttimeout = lastRead .Add (d .EscTimeout ) 219n := d .sendEvents (eventc ,buf .Bytes (),false ) 220if ! timeout .Stop () { 221// drain the channel if it was already running 222select { 223case <- timeout .C : 224default : 225 } 226 } 227 228if n > 0 { 229d .logf ("processed %d bytes from buffer" ,n ) 230buf .Next (n ) 231 } 232 233if buf .Len ()> 0 { 234d .logf ("resetting timeout for remaining buffer after parse" ) 235timeout .Reset (d .EscTimeout ) 236 } 237 } 238 } 239} 240 241// awaitingClipboardReply reports whether buf starts an OSC 5522 reply whose 242// rest may still be on its way. 243func awaitingClipboardReply (buf []byte ,lastRead time.Time )bool { 244return len (buf )<= maxPendingClipboardOsc && time .Since (lastRead )< pendingClipboardOscTimeout && 245bytes .HasPrefix (buf , []byte ("\x1b]5522;" )) 246} 247 248// SetLogger sets the logger to use for debugging. If nil, no logging will be 249// performed. 250func (d * TerminalReader )SetLogger (logger Logger ) { 251d .logger = logger 252} 253 254func (d * TerminalReader )sendEvents (eventc chan <- Event ,buf []byte ,expired bool )int { 255n ,events := d .eventScanner .scanEvents (buf ,expired ) 256for _ ,event := range events { 257eventc <- event 258 } 259return n 260} 261 262// eventScanner scans the buffer for events and sends them to the event channel. 263type eventScanner struct { 264EventDecoder 265 266utf16Half [2 ]bool // 0 key up, 1 key down 267utf16Buf [2 ][2 ]rune // 0 key up, 1 key down 268graphemeBuf [2 ][]rune // 0 key up, 1 key down 269paste []byte 270table map [string ]Key 271lookup bool 272logger Logger 273} 274 275// newEventScanner creates a new event scanner. 276func newEventScanner ()* eventScanner { 277return & eventScanner {} 278} 279 280// setLogger sets the logger to use for debugging. If nil, no logging will be 281// performed. 282func (d * eventScanner )setLogger (logger Logger ) { 283d .logger = logger 284} 285 286func (d * eventScanner )logf (format string ,v ... interface {}) { 287logf (d .logger ,format ,v ... ) 288} 289 290func (d * eventScanner )scanEvents (buf []byte ,expired bool ) (total int ,events []Event ) { 291if len (buf )== 0 { 292return 0 ,nil 293 } 294 295var dn int 296d .logf ("processing buf %d %q" ,len (buf ),buf ) 297dn ,buf = d .deserializeWin32Input (buf ) 298total += dn 299 300// Lookup table first 301if d .lookup && len (buf )> 2 && buf [0 ]== ansi .ESC { 302if k ,ok := d .table [string (buf )];ok { 303return len (buf ), []Event {KeyPressEvent (k )} 304 } 305 } 306 307// total is the total number of bytes processed 308for len (buf )> 0 { 309esc := buf [0 ]== ansi .ESC 310n ,event := d .Decode (buf ) 311 312// Handle bracketed-paste 313if d .paste != nil {//nolint:nestif 314if _ ,ok := event .(PasteEndEvent );! ok { 315switch event := event .(type ) { 316case KeyPressEvent : 317if len (event .Text )> 0 { 318d .paste = append (d .paste ,event .Text ... ) 319 }else { 320seq := string (buf [:n ]) 321isWin32 := strings .HasPrefix (seq ,"\x1b[" )&& strings .HasSuffix (seq ,"_" ) 322switch { 323case isWin32 && event .Code == KeyEnter && event .Code == event .BaseCode : 324// This handles special cases where 325// win32-input-mode encodes newlines and other keys 326// as keypress events. We need to encode them as 327// their respective values. 328d .paste = append (d .paste ,'\n' ) 329case isWin32 && unicode .IsControl (event .Code )&& event .Code == event .BaseCode : 330// This handles other cases such as tabs, escapes, etc. 331d .paste = append (d .paste ,string (event .Code )... ) 332case ! isWin32 : 333// We ignore all other non-text win32-input-mode events. 334if esc && n <= 2 && ! expired { 335// If the event is an escape sequence and we 336// are not expired, we need to wait for more 337// input. 338return total ,events 339 } 340d .paste = append (d .paste ,seq ... ) 341 } 342 } 343case UnknownEvent : 344if ! expired { 345// If the event is unknown and we are not expired, we 346// return need to try to decode the buffer again. 347return total ,events 348 } 349default : 350// Everything else is ignored... 351 } 352buf = buf [n :] 353total += n 354continue 355 } 356 } 357 358var isUnknown bool 359switch event .(type ) { 360case ignoredEvent : 361// ignore this event 362event = nil 363case UnknownEvent : 364isUnknown = true 365// Try to look up the event in the table. 366if ! expired { 367return total ,events 368 } 369 370if k ,ok := d .table [string (buf [:n ])];ok { 371events = append (events ,KeyPressEvent (k )) 372return total + n ,events 373 } 374 375events = append (events ,event ) 376case PasteStartEvent : 377d .paste = []byte {}// reset the paste buffer 378case PasteEndEvent : 379var paste []rune 380for len (d .paste )> 0 { 381r ,w := utf8 .DecodeRune (d .paste ) 382if r != utf8 .RuneError { 383paste = append (paste ,r ) 384 } 385d .paste = d .paste [w :] 386 } 387d .paste = nil // reset the paste buffer 388events = append (events ,PasteEvent {string (paste )}) 389 } 390 391if ! isUnknown && event != nil { 392if esc && n <= 2 && ! expired { 393// Wait for more input 394return total ,events 395 } 396 397if m ,ok := event .(MultiEvent );ok { 398// If the event is a MultiEvent, append all events to the queue. 399events = append (events ,m ... ) 400 }else { 401// Otherwise, just append the event to the queue. 402events = append (events ,event ) 403 } 404 } 405 406buf = buf [n :] 407total += n 408 } 409 410return total ,events 411} 412 413func (d * eventScanner )encodeGraphemeBufs () []byte { 414var b []byte 415for kd := range d .graphemeBuf { 416if len (d .graphemeBuf [kd ])> 0 { 417switch kd { 418case 1 : 419b = append (b ,string (d .graphemeBuf [kd ])... ) 420case 0 : 421// Encode the release grapheme as Kitty Keyboard to get the release event. 422grs := uniseg .NewGraphemes (string (d .graphemeBuf [kd ])) 423for grs .Next () { 424var codepoints string 425gr := grs .Str () 426for i ,r := range gr { 427if r == 0 { 428continue 429 } 430if i > 0 { 431codepoints += ":" 432 } 433codepoints += strconv .FormatInt (int64 (r ),10 ) 434 } 435// This is dark :) 436// During serializing/deserializing of win32 input events, the 437// API will split a grapheme into runes and send them as a 438// keydown/keyup sequences. We collect the runes, decoded them 439// from UTF-16 just fine. However, [EventDecoder.Decode] will 440// always decode graphemes as [KeyPressEvent]s. Thus, to 441// workaround that and the existing API, while we are 442// intercepting win32 input events, we encode the grapheme 443// release events as Kitty Keyboard sequences so that 444// [EventDecoder.Decode] can properly decode them as 445// [KeyReleaseEvent]s. 446seq := fmt .Sprintf ("\x1b[%d;1:3;%su" ,d .graphemeBuf [kd ][0 ],codepoints ) 447b = append (b ,seq ... ) 448 } 449 } 450d .graphemeBuf [kd ]= d .graphemeBuf [kd ][:0 ]// reset the buffer 451 } 452 } 453return b 454} 455 456func (d * eventScanner )storeGraphemeRune (kd int ,r rune ) { 457if d .utf16Half [kd ] { 458// We have a half pair that needs to be decoded. 459d .utf16Half [kd ]= false 460d .utf16Buf [kd ][1 ]= r 461r := utf16 .DecodeRune (d .utf16Buf [kd ][0 ],d .utf16Buf [kd ][1 ]) 462d .graphemeBuf [kd ]= append (d .graphemeBuf [kd ],r ) 463 }else if utf16 .IsSurrogate (r ) { 464// This is the first half of a UTF-16 surrogate pair. 465d .utf16Half [kd ]= true 466d .utf16Buf [kd ][0 ]= r 467 }else { 468// This should be a single UTF-16 that can be converted 469// to UTF-8. 470d .graphemeBuf [kd ]= append (d .graphemeBuf [kd ],r ) 471 } 472} 473 474// deserializeWin32Input deserializes the Win32 input events converting 475// KeyEventRecrods to bytes. Before returning the bytes, it will also try to 476// decode any UTF-16 pairs that might be present in the input buffer. 477func (d * eventScanner )deserializeWin32Input (buf []byte ) (int , []byte ) { 478p := ansi .GetParser () 479defer ansi .PutParser (p ) 480 481var processed int 482var state byte 483des := make ([]byte ,0 ,len (buf )) 484 485for len (buf )> 0 { 486seq ,width ,n ,newState := ansi .DecodeSequence (buf ,state ,p ) 487switch width { 488case 0 : 489if p .Command ()== '_' {// Win32 Input Mode 490vk ,_ := p .Param (0 ,0 ) 491if vk == 0 { 492// This is either a serialized KeyEventRecord or a UTF-16 493// pair. 494uc ,_ := p .Param (2 ,0 ) 495kd ,_ := p .Param (3 ,0 ) 496kd = clamp (kd ,0 ,1 )// kd is the key down state (0 or 1) 497d .storeGraphemeRune (kd ,rune (uc )) 498processed += n 499break 500 } 501 } 502fallthrough 503default : 504des = append (des ,d .encodeGraphemeBufs ()... ) 505des = append (des ,seq ... ) 506 } 507 508state = newState 509buf = buf [n :] 510 } 511 512des = append (des ,d .encodeGraphemeBufs ()... ) 513 514return processed ,des 515} 516 517func (d * TerminalReader )logf (format string ,v ... interface {}) { 518logf (d .logger ,format ,v ... ) 519} 520 521func logf (logger Logger ,format string ,v ... interface {}) { 522if logger == nil { 523return 524 } 525logger .Printf (format ,v ... ) 526}