mirror of
https://github.com/junegunn/fzf.git
synced 2026-10-10 07:15:58 +08:00
Drop unrecognized escape sequences instead of typing them
- Frame CSI by parameter and final byte ranges, string sequences (OSC, DCS, APC) by ST or BEL terminator - Consume framed sequence whole. Partial consumption left remainder to be read as input: CTRL-A as \e[97;5u typed "97;5u" into query - BEL-terminated OSC reply was worse than junk. Payload typed, then BEL read as CTRL-G, aborting fzf. \e]0;title\a was enough - Wait for string terminator in read loop, as already done for CSI - Take second-chance read only while sequence unfinished. Re-reading after complete one blocked until next keystroke, holding back what followed - Leave unterminated sequences alone. That is how ALT-[ and ALT-] arrive - Skip SOS and PM. Nothing sends them, and waiting for a terminator that never comes would delay ALT-X and ALT-^
This commit is contained in:
3 files changed
+208
-2
No files matched your search
+86
-2
@@ -348,6 +348,53 @@ func getEnv(name string, defaultValue int) int {
|
||||
func csiContinues(b byte) bool { return b >= 0x20 && b <= 0x3f }
|
||||
func csiFinal(b byte) bool { return b >= 0x40 && b <= 0x7e }
|
||||
|
||||
// csiEnd returns the length of the CSI sequence at the start of the buffer, or
|
||||
// 0 if it has no final byte yet or is malformed.
|
||||
func csiEnd(buffer []byte) int {
|
||||
for i := 2; i < len(buffer); i++ {
|
||||
if csiFinal(buffer[i]) {
|
||||
return i + 1
|
||||
}
|
||||
if !csiContinues(buffer[i]) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// stringEnd returns the length of the string sequence (DCS, OSC or APC) at the
|
||||
// start of the buffer, or 0 if its terminator has not arrived. The terminator is
|
||||
// ST, and BEL as well because xterm has always allowed it.
|
||||
func stringEnd(buffer []byte) int {
|
||||
for i := 2; i < len(buffer); i++ {
|
||||
switch buffer[i] {
|
||||
case '\a':
|
||||
return i + 1
|
||||
case Esc.Byte():
|
||||
if i+1 < len(buffer) && buffer[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
// A lone ESC is either a truncated payload or a terminator still
|
||||
// arriving. Either way the sequence is not framed.
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// stringIntroducer reports whether the byte after ESC starts a string sequence.
|
||||
// Only the three that terminals actually reply with: OSC for colors, title and
|
||||
// clipboard, DCS for XTVERSION and XTGETTCAP, APC for Kitty graphics. SOS and PM
|
||||
// are left out, as nothing sends them and waiting for a terminator that will
|
||||
// never come would delay ALT-X and ALT-^.
|
||||
func stringIntroducer(b byte) bool {
|
||||
switch b {
|
||||
case 'P', ']', '_':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// incompleteEscape reports whether the buffer ends in an escape sequence that
|
||||
// has not been terminated yet. The read loop keeps waiting in that case, so the
|
||||
// parser is never handed a fragment to guess at.
|
||||
@@ -376,6 +423,9 @@ func incompleteEscape(buffer []byte) bool {
|
||||
case 'O':
|
||||
return len(tail)-start < 3
|
||||
}
|
||||
if stringIntroducer(tail[start+1]) {
|
||||
return stringEnd(tail[start:]) == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -483,8 +533,10 @@ func (r *LightRenderer) GetChar(cancellable bool) Event {
|
||||
return Event{CtrlSlash, 0, nil}
|
||||
case Esc.Byte():
|
||||
ev := r.escSequence(&sz)
|
||||
// Second chance
|
||||
if ev.Type == Invalid {
|
||||
// Second chance, but only for a sequence that has not finished
|
||||
// arriving. Re-reading after a complete one blocks until the next
|
||||
// keystroke, holding back whatever follows it in the buffer.
|
||||
if ev.Type == Invalid && incompleteEscape(r.buffer) {
|
||||
r.buffer, result, err = r.getBytes(true)
|
||||
if err != nil {
|
||||
return Event{Fatal, 0, nil}
|
||||
@@ -525,7 +577,23 @@ func (r *LightRenderer) setCancel(f func()) {
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
// escSequence parses an escape sequence. A CSI sequence fzf has no event for is
|
||||
// dropped whole: consuming only the part that parsed leaves the rest to be read
|
||||
// as input and typed into the query.
|
||||
func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
ev := r.parseEscSequence(sz)
|
||||
if ev.Type != Invalid || len(r.buffer) < 3 || r.buffer[1] != '[' {
|
||||
return ev
|
||||
}
|
||||
// Only a framed sequence is dropped. One still missing its final byte may
|
||||
// yet be arriving, and the caller gives it another chance.
|
||||
if end := csiEnd(r.buffer); end > *sz {
|
||||
*sz = end
|
||||
}
|
||||
return ev
|
||||
}
|
||||
|
||||
func (r *LightRenderer) parseEscSequence(sz *int) Event {
|
||||
if len(r.buffer) < 2 {
|
||||
return Event{Esc, 0, nil}
|
||||
}
|
||||
@@ -987,6 +1055,22 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
} // r.buffer[2]
|
||||
} // r.buffer[2]
|
||||
} // r.buffer[1]
|
||||
// Nothing matched. A framed sequence is dropped whole: reading its
|
||||
// introducer as an ALT-key below would type the rest into the query.
|
||||
// Unterminated ones are left alone, as that is how ALT-[ and ALT-] arrive.
|
||||
if r.buffer[1] == '[' {
|
||||
// A bare "\e[c" is ALT-[ followed by a character, not a CSI sequence
|
||||
if end := csiEnd(r.buffer); end > 3 {
|
||||
*sz = end
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
} else if stringIntroducer(r.buffer[1]) {
|
||||
if end := stringEnd(r.buffer); end > 0 {
|
||||
*sz = end
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
}
|
||||
|
||||
rest := bytes.NewBuffer(r.buffer[1:])
|
||||
c, size, err := rest.ReadRune()
|
||||
if err == nil {
|
||||
|
||||
Reference in new issue
Block a user