enabled multi-line paste
This commit is contained in:
@@ -2230,13 +2230,14 @@ func visibleLen(s string) int {
|
||||
}
|
||||
|
||||
type editor struct {
|
||||
prompt string
|
||||
buf []rune
|
||||
pos int
|
||||
hist []string
|
||||
hpos int
|
||||
draft string
|
||||
crow int
|
||||
prompt string
|
||||
buf []rune
|
||||
pos int
|
||||
hist []string
|
||||
hpos int
|
||||
draft string
|
||||
crow int
|
||||
pasting bool // true while inside a bracketed paste (ESC[200~ .. ESC[201~)
|
||||
}
|
||||
|
||||
func textPos(promptLen, W int, s string, pos int) (row, col int) {
|
||||
@@ -2367,6 +2368,181 @@ func readPlain(prompt string) (string, bool) {
|
||||
return strings.TrimRight(line, "\r\n"), true
|
||||
}
|
||||
|
||||
// keyKind classifies what readLine should do after handling one input rune.
|
||||
type keyKind int
|
||||
|
||||
const (
|
||||
keyNone keyKind = iota // keep editing
|
||||
keySubmit // submit the current buffer
|
||||
keyAbort // interrupt (Ctrl+C)
|
||||
keyEOF // end of input (Ctrl+D on an empty buffer)
|
||||
)
|
||||
|
||||
// insert places r at the cursor, shifting the tail right.
|
||||
func (e *editor) insert(r rune) {
|
||||
e.buf = append(e.buf, 0)
|
||||
copy(e.buf[e.pos+1:], e.buf[e.pos:])
|
||||
e.buf[e.pos] = r
|
||||
e.pos++
|
||||
}
|
||||
|
||||
// handleCSI interprets a complete CSI sequence (the bytes between ESC[ and its
|
||||
// final byte): cursor movement, history and bracketed-paste markers.
|
||||
func (e *editor) handleCSI(seq []byte) {
|
||||
if len(seq) == 0 {
|
||||
return
|
||||
}
|
||||
switch seq[len(seq)-1] {
|
||||
case 'A':
|
||||
e.histNav(true)
|
||||
case 'B':
|
||||
e.histNav(false)
|
||||
case 'C':
|
||||
if strings.Contains(string(seq), "5") {
|
||||
e.wordFwd() // Ctrl+Right
|
||||
} else if e.pos < len(e.buf) {
|
||||
e.pos++
|
||||
}
|
||||
case 'D':
|
||||
if strings.Contains(string(seq), "5") {
|
||||
e.wordBack() // Ctrl+Left
|
||||
} else if e.pos > 0 {
|
||||
e.pos--
|
||||
}
|
||||
case 'H':
|
||||
e.pos = 0 // Home (ESC[H)
|
||||
case 'F':
|
||||
e.pos = len(e.buf) // End (ESC[F)
|
||||
case '~':
|
||||
s := string(seq)
|
||||
if strings.HasSuffix(s, "200~") {
|
||||
e.pasting = true // bracketed paste start: ESC[200~
|
||||
} else if strings.HasSuffix(s, "201~") {
|
||||
e.pasting = false // bracketed paste end: ESC[201~
|
||||
} else if seq[0] == '1' {
|
||||
e.pos = 0 // Home (ESC[1~)
|
||||
} else if seq[0] == '4' {
|
||||
e.pos = len(e.buf) // End (ESC[4~)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swallowLF consumes a single buffered LF so CRLF input is not counted as two
|
||||
// separate line breaks.
|
||||
func (e *editor) swallowLF(r *bufio.Reader) {
|
||||
if r.Buffered() > 0 {
|
||||
if b, _ := r.Peek(1); len(b) > 0 && b[0] == '\n' {
|
||||
r.ReadByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// feed processes one decoded key. r supplies the remainder of an escape
|
||||
// sequence. It returns the action the REPL loop must take next. Keeping the key
|
||||
// logic here, rather than inline in readLine, makes multi-line paste handling
|
||||
// unit-testable without a real terminal.
|
||||
func (e *editor) feed(rn rune, r *bufio.Reader) keyKind {
|
||||
switch rn {
|
||||
case '\r':
|
||||
// Inside a bracketed paste a CR is a literal line break; only a real
|
||||
// Enter (outside a paste) submits the prompt.
|
||||
e.swallowLF(r)
|
||||
if e.pasting {
|
||||
e.insert('\n')
|
||||
return keyNone
|
||||
}
|
||||
return keySubmit
|
||||
case '\n':
|
||||
e.insert('\n')
|
||||
case 0x03:
|
||||
if e.pasting {
|
||||
return keyNone // ignore a stray ^C inside a pasted block
|
||||
}
|
||||
return keyAbort
|
||||
case 0x04:
|
||||
if len(e.buf) == 0 {
|
||||
return keyEOF
|
||||
}
|
||||
// Ctrl+D with non-empty buffer: delete the character under the cursor
|
||||
if e.pos < len(e.buf) {
|
||||
e.buf = append(e.buf[:e.pos], e.buf[e.pos+1:]...)
|
||||
}
|
||||
case 0x01:
|
||||
e.pos = 0 // Ctrl+A: beginning of line
|
||||
case 0x05:
|
||||
e.pos = len(e.buf) // Ctrl+E: end of line
|
||||
case 0x02:
|
||||
if e.pos > 0 {
|
||||
e.pos--
|
||||
} // Ctrl+B: backward char
|
||||
case 0x06:
|
||||
if e.pos < len(e.buf) {
|
||||
e.pos++
|
||||
} // Ctrl+F: forward char
|
||||
case 0x17:
|
||||
e.delWordBack() // Ctrl+W: delete previous word
|
||||
case 0x0b:
|
||||
e.buf = e.buf[:e.pos] // Ctrl+K: kill to end of line
|
||||
case 0x15:
|
||||
e.buf = e.buf[e.pos:] // Ctrl+U: kill to start of line
|
||||
e.pos = 0
|
||||
case 0x7f, 0x08:
|
||||
if e.pos > 0 {
|
||||
e.buf = append(e.buf[:e.pos-1], e.buf[e.pos:]...)
|
||||
e.pos--
|
||||
}
|
||||
case 0x1b:
|
||||
b1, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return keyNone
|
||||
}
|
||||
if b1 == '[' {
|
||||
// Read the full CSI sequence (terminated by a byte in 0x40-0x7e).
|
||||
var seq []byte
|
||||
for {
|
||||
b, err := r.ReadByte()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
seq = append(seq, b)
|
||||
if b >= 0x40 && b <= 0x7e {
|
||||
break
|
||||
}
|
||||
}
|
||||
e.handleCSI(seq)
|
||||
} else if b1 == 'O' {
|
||||
// xterm application cursor keys: ESC O H/F (Home/End), A-D (arrows)
|
||||
b2, err2 := r.ReadByte()
|
||||
if err2 != nil {
|
||||
return keyNone
|
||||
}
|
||||
switch b2 {
|
||||
case 'H':
|
||||
e.pos = 0
|
||||
case 'F':
|
||||
e.pos = len(e.buf)
|
||||
case 'A':
|
||||
e.histNav(true)
|
||||
case 'B':
|
||||
e.histNav(false)
|
||||
case 'C':
|
||||
if e.pos < len(e.buf) {
|
||||
e.pos++
|
||||
}
|
||||
case 'D':
|
||||
if e.pos > 0 {
|
||||
e.pos--
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
if rn >= 32 || rn == '\t' {
|
||||
e.insert(rn)
|
||||
}
|
||||
}
|
||||
return keyNone
|
||||
}
|
||||
|
||||
func readLine(prompt string) (string, bool) {
|
||||
if !isTerminal(int(os.Stdin.Fd())) {
|
||||
return readPlain(prompt)
|
||||
@@ -2376,6 +2552,11 @@ func readLine(prompt string) (string, bool) {
|
||||
return readPlain(prompt)
|
||||
}
|
||||
defer restore()
|
||||
// Enable bracketed paste for the duration of editing: pasted blocks then
|
||||
// arrive wrapped in ESC[200~ .. ESC[201~, letting feed() treat their
|
||||
// embedded newlines as literal line breaks instead of submitting mid-paste.
|
||||
fmt.Print("\033[?2004h")
|
||||
defer fmt.Print("\033[?2004l")
|
||||
e := &editor{prompt: prompt, hpos: -1, hist: hist}
|
||||
for {
|
||||
e.draw()
|
||||
@@ -2384,13 +2565,8 @@ func readLine(prompt string) (string, bool) {
|
||||
fmt.Print("\r\n")
|
||||
return "", false
|
||||
}
|
||||
switch rn {
|
||||
case '\r':
|
||||
if stdin.Buffered() > 0 {
|
||||
if b, _ := stdin.Peek(1); len(b) > 0 && b[0] == '\n' {
|
||||
stdin.ReadByte()
|
||||
}
|
||||
}
|
||||
switch e.feed(rn, stdin) {
|
||||
case keySubmit:
|
||||
W := termWidth()
|
||||
s := string(e.buf)
|
||||
P := visibleLen(e.prompt)
|
||||
@@ -2401,130 +2577,12 @@ func readLine(prompt string) (string, bool) {
|
||||
}
|
||||
fmt.Print("\r\n")
|
||||
return string(e.buf), true
|
||||
case '\n':
|
||||
e.buf = append(e.buf, 0)
|
||||
copy(e.buf[e.pos+1:], e.buf[e.pos:])
|
||||
e.buf[e.pos] = '\n'
|
||||
e.pos++
|
||||
case 0x03:
|
||||
case keyAbort:
|
||||
fmt.Print("^C\r\n")
|
||||
return "", true
|
||||
case 0x04:
|
||||
if len(e.buf) == 0 {
|
||||
fmt.Print("\r\n")
|
||||
return "", false
|
||||
}
|
||||
// Ctrl+D with non-empty buffer: delete character under cursor
|
||||
if e.pos < len(e.buf) {
|
||||
e.buf = append(e.buf[:e.pos], e.buf[e.pos+1:]...)
|
||||
}
|
||||
case 0x01:
|
||||
e.pos = 0 // Ctrl+A: beginning of line
|
||||
case 0x05:
|
||||
e.pos = len(e.buf) // Ctrl+E: end of line
|
||||
case 0x02:
|
||||
if e.pos > 0 {
|
||||
e.pos--
|
||||
} // Ctrl+B: backward char
|
||||
case 0x06:
|
||||
if e.pos < len(e.buf) {
|
||||
e.pos++
|
||||
} // Ctrl+F: forward char
|
||||
case 0x17:
|
||||
e.delWordBack() // Ctrl+W: delete previous word
|
||||
case 0x0b:
|
||||
e.buf = e.buf[:e.pos] // Ctrl+K: kill to end of line
|
||||
case 0x15:
|
||||
e.buf = e.buf[e.pos:] // Ctrl+U: kill to start of line
|
||||
e.pos = 0
|
||||
case 0x7f, 0x08:
|
||||
if e.pos > 0 {
|
||||
e.buf = append(e.buf[:e.pos-1], e.buf[e.pos:]...)
|
||||
e.pos--
|
||||
}
|
||||
case 0x1b:
|
||||
b1, err1 := stdin.ReadByte()
|
||||
if err1 != nil {
|
||||
continue
|
||||
}
|
||||
if b1 == '[' {
|
||||
// Read the full CSI sequence (terminated by a byte in 0x40-0x7e).
|
||||
var seq []byte
|
||||
for {
|
||||
b, err := stdin.ReadByte()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
seq = append(seq, b)
|
||||
if b >= 0x40 && b <= 0x7e {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(seq) == 0 {
|
||||
continue
|
||||
}
|
||||
last := seq[len(seq)-1]
|
||||
switch last {
|
||||
case 'A':
|
||||
e.histNav(true)
|
||||
case 'B':
|
||||
e.histNav(false)
|
||||
case 'C':
|
||||
if strings.Contains(string(seq), "5") {
|
||||
e.wordFwd() // Ctrl+Right
|
||||
} else if e.pos < len(e.buf) {
|
||||
e.pos++
|
||||
}
|
||||
case 'D':
|
||||
if strings.Contains(string(seq), "5") {
|
||||
e.wordBack() // Ctrl+Left
|
||||
} else if e.pos > 0 {
|
||||
e.pos--
|
||||
}
|
||||
case 'H':
|
||||
e.pos = 0 // Home (ESC[H)
|
||||
case 'F':
|
||||
e.pos = len(e.buf) // End (ESC[F)
|
||||
case '~':
|
||||
// Home/End on some terminals: ESC[1~ / ESC[4~
|
||||
if len(seq) >= 1 && seq[0] == '1' {
|
||||
e.pos = 0
|
||||
} else if len(seq) >= 1 && seq[0] == '4' {
|
||||
e.pos = len(e.buf)
|
||||
}
|
||||
}
|
||||
} else if b1 == 'O' {
|
||||
// xterm application cursor keys: ESC O H/F (Home/End), A-D (arrows)
|
||||
b2, err2 := stdin.ReadByte()
|
||||
if err2 != nil {
|
||||
continue
|
||||
}
|
||||
switch b2 {
|
||||
case 'H':
|
||||
e.pos = 0
|
||||
case 'F':
|
||||
e.pos = len(e.buf)
|
||||
case 'A':
|
||||
e.histNav(true)
|
||||
case 'B':
|
||||
e.histNav(false)
|
||||
case 'C':
|
||||
if e.pos < len(e.buf) {
|
||||
e.pos++
|
||||
}
|
||||
case 'D':
|
||||
if e.pos > 0 {
|
||||
e.pos--
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
if rn >= 32 {
|
||||
e.buf = append(e.buf, 0)
|
||||
copy(e.buf[e.pos+1:], e.buf[e.pos:])
|
||||
e.buf[e.pos] = rn
|
||||
e.pos++
|
||||
}
|
||||
case keyEOF:
|
||||
fmt.Print("\r\n")
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2717,7 +2775,7 @@ func main() {
|
||||
if re == "" {
|
||||
re = "high"
|
||||
}
|
||||
fmt.Println(c("Bantam Agent ready", 1, 32) + c(" (Ctrl+J = new line)", 2))
|
||||
fmt.Println(c("Bantam Agent ready", 1, 32) + c(" (Enter submits; Ctrl+J or paste = new line)", 2))
|
||||
fmt.Println(c(fmt.Sprintf("endpoint: %s model: %s temp: %v reasoning: %s context: %d", cfg.Endpoint, cfg.Model, cfg.Temperature, re, cfg.ContextWindow), 2))
|
||||
for {
|
||||
u, ok := readLine(c("> ", 1, 36))
|
||||
|
||||
Reference in New Issue
Block a user