Compare commits

..

19 Commits

Author SHA1 Message Date
Gopher Robot
a7e5b0437f go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I5ffb7b7c88fd3074335b3465a0fb76e839654802
Reviewed-on: https://go-review.googlesource.com/c/term/+/734802
Reviewed-by: Cherry Mui <cherryyz@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Junyang Shao <shaojunyang@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
2026-01-09 08:09:29 -08:00
Josh Bleecher Snyder
943f25d359 x/term: handle transpose
The behavior implemented here matches readline and libedit.

Updates golang/go#76826

Change-Id: I893677f9bceaf75aa1dada7d893845728e07057e
Reviewed-on: https://go-review.googlesource.com/c/term/+/730441
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Reviewed-by: David Chase <drchase@google.com>
2025-12-17 11:04:51 -08:00
Josh Bleecher Snyder
9b991dd831 x/term: handle delete key
Fixes golang/go#76826

Change-Id: I80dc9468c2cc716dab10a8e165ca5f72817ef4b9
Reviewed-on: https://go-review.googlesource.com/c/term/+/730440
Auto-Submit: Michael Knyszek <mknyszek@google.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2025-12-16 13:07:46 -08:00
Gopher Robot
3863673230 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I2b01858489f549d13f8274173247152bd9067849
Reviewed-on: https://go-review.googlesource.com/c/term/+/728003
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2025-12-08 08:57:31 -08:00
Gopher Robot
1231d5465b go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I2c69a9e891bebfbb3d88ab32d8a7dc2973440130
Reviewed-on: https://go-review.googlesource.com/c/term/+/718961
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2025-11-11 07:03:38 -08:00
cuishuang
3475bc8ef1 term: fix some comments
Change-Id: I8211158071d0a657d0097aba242278fa4bc7f653
Reviewed-on: https://go-review.googlesource.com/c/term/+/713420
Auto-Submit: Sean Liao <sean@liao.dev>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Sean Liao <sean@liao.dev>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2025-10-23 11:33:45 -07:00
Gopher Robot
3a0828a666 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I91b80d2350746afd3a4a6c03bc55eeb03a42e6ae
Reviewed-on: https://go-review.googlesource.com/c/term/+/710095
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: David Chase <drchase@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2025-10-08 08:23:35 -07:00
Gopher Robot
1a11b45a6f go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: If0cee55db64c734f53dfaa2d5b014c7eba22cd46
Reviewed-on: https://go-review.googlesource.com/c/term/+/701037
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2025-09-07 20:32:07 -07:00
Gopher Robot
d862cd548e all: upgrade go directive to at least 1.24.0 [generated]
By now Go 1.25.0 has been released, and Go 1.23 is no longer supported
per the Go Release Policy (see https://go.dev/doc/devel/release#policy).

For golang/go#69095.

[git-generate]
(cd . && go get go@1.24.0 && go mod tidy && go fix ./... && go mod edit -toolchain=none)

Change-Id: I8bf28e20963f73333d83fbe50434bb5abcce36da
Reviewed-on: https://go-review.googlesource.com/c/term/+/695280
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2025-08-13 07:50:04 -07:00
Gopher Robot
a35244d18d go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I99ad2113f2fb7d1f8e22475659335424ff94ffb7
Reviewed-on: https://go-review.googlesource.com/c/term/+/693976
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2025-08-07 07:52:19 -07:00
Laurent Demailly
4f53e0cd39 term: allow multi-line bracketed paste to not create single line with verbatim LFs
Treat "\n" (LF) like "Enter" (CR)

Avoids that when pasting 3 lines
(with a terminal like kitty, ghostty, alacritty that do not change the clipboard
in bracketed paste mode)
it turns into 1 prompt looking like:

Test> line one
..............line.two
......................line.three

Fixes golang/go#74600

Change-Id: I4a86044a4a175eccb3a96dbf7021fee97a5940ce
GitHub-Last-Rev: 0cf26df9ae
GitHub-Pull-Request: golang/term#21
Reviewed-on: https://go-review.googlesource.com/c/term/+/687755
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2025-07-21 12:13:29 -07:00
Ayman Bagabas
27f29d8328 term: remove duplicate flag and add comment on windows
Both windows.ENABLE_PROCESSED_INPUT and windows.ENABLE_PROCESSED_OUTPUT have
the same value of 0x1. Using makeRaw on a console output screen buffer handle
wouldn't make sense since on Windows the input handle and the output screen
buffer handle are two separate things.

See https://learn.microsoft.com/en-us/windows/console/setconsolemode

Change-Id: I19feadf8da303a1ada8e517399416a4730845900
GitHub-Last-Rev: 832fc7e8af
GitHub-Pull-Request: golang/term#14
Reviewed-on: https://go-review.googlesource.com/c/term/+/562779
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Laurent Demailly <ldemailly@gmail.com>
Reviewed-by: Jorropo <jorropo.pgm@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
2025-07-15 09:43:07 -07:00
Gopher Robot
30da5dd58f go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I4d4074eba1258708fcf00744be2607aadb104396
Reviewed-on: https://go-review.googlesource.com/c/term/+/687037
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2025-07-09 13:10:58 -07:00
Gopher Robot
2ec7864a3e go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I044141b4808276d77216779c66fa057da6143ad3
Reviewed-on: https://go-review.googlesource.com/c/term/+/669916
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: David Chase <drchase@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2025-05-05 11:12:45 -07:00
Laurent Demailly
a809085bff term: support pluggable history
Expose a new History interface that allows replacement of the default
ring buffer to customize what gets added or not; as well as to allow
saving/restoring history on either the default ringbuffer or a custom
replacement.

Fixes golang/go#68780

Change-Id: I7e61dc6bb438749c8502223705518ef8ff9025b4
GitHub-Last-Rev: 621281355f
GitHub-Pull-Request: golang/term#20
Reviewed-on: https://go-review.googlesource.com/c/term/+/659835
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Jorropo <jorropo.pgm@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Jorropo <jorropo.pgm@gmail.com>
Reviewed-by: Austin Clements <austin@google.com>
2025-04-21 12:30:57 -07:00
Gopher Robot
5d2308b09d go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: I84956d670066c4699c9f1167d8d5f64111108109
Reviewed-on: https://go-review.googlesource.com/c/term/+/663095
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: David Chase <drchase@google.com>
2025-04-05 17:33:08 -07:00
Dragos Vingarzan
e770dddbf5 x/term: disabling auto-completion around GetPassword()
Triggering the completion during password input might cause some
unintended behavior around handling of TAB, or maybe the auto-completion
functionality would review the secret input. Hence simply
disabling/re-enabling it around the t.readLine call.

Fixes #72736

Change-Id: I64270e8570086247247466afb2536b2581d6af25
Reviewed-on: https://go-review.googlesource.com/c/term/+/607115
Reviewed-by: David Chase <drchase@google.com>
Reviewed-by: Keith Randall <khr@google.com>
Auto-Submit: Sean Liao <sean@liao.dev>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
2025-03-15 12:59:10 -07:00
Gopher Robot
04218fdaf7 go.mod: update golang.org/x dependencies
Update golang.org/x dependencies to their latest tagged versions.

Change-Id: Iaed852644381f73352fe26488aa2eb82f4b68c4e
Reviewed-on: https://go-review.googlesource.com/c/term/+/655035
Auto-Submit: Gopher Robot <gobot@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: David Chase <drchase@google.com>
2025-03-05 07:52:00 -08:00
Gopher Robot
208db03875 all: upgrade go directive to at least 1.23.0 [generated]
By now Go 1.24.0 has been released, and Go 1.22 is no longer supported
per the Go Release Policy (https://go.dev/doc/devel/release#policy).

For golang/go#69095.

[git-generate]
(cd . && go get go@1.23.0 && go mod tidy && go fix ./... && go mod edit -toolchain=none)

Change-Id: Ia92133521e64a509dbf90ea4945a0c943b961cb2
Reviewed-on: https://go-review.googlesource.com/c/term/+/649398
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Auto-Submit: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
2025-02-14 13:48:08 -08:00
5 changed files with 266 additions and 21 deletions

4
go.mod
View File

@@ -1,5 +1,5 @@
module golang.org/x/term
go 1.18
go 1.24.0
require golang.org/x/sys v0.30.0
require golang.org/x/sys v0.40.0

4
go.sum
View File

@@ -1,2 +1,2 @@
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

View File

@@ -20,12 +20,14 @@ func isTerminal(fd int) bool {
return err == nil
}
// This is intended to be used on a console input handle.
// See https://learn.microsoft.com/en-us/windows/console/setconsolemode
func makeRaw(fd int) (*State, error) {
var st uint32
if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil {
return nil, err
}
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT)
raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT)
raw |= windows.ENABLE_VIRTUAL_TERMINAL_INPUT
if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil {
return nil, err

View File

@@ -6,6 +6,7 @@ package term
import (
"bytes"
"fmt"
"io"
"runtime"
"strconv"
@@ -36,6 +37,26 @@ var vt100EscapeCodes = EscapeCodes{
Reset: []byte{keyEscape, '[', '0', 'm'},
}
// A History provides a (possibly bounded) queue of input lines read by [Terminal.ReadLine].
type History interface {
// Add will be called by [Terminal.ReadLine] to add
// a new, most recent entry to the history.
// It is allowed to drop any entry, including
// the entry being added (e.g., if it's deemed an invalid entry),
// the least-recent entry (e.g., to keep the history bounded),
// or any other entry.
Add(entry string)
// Len returns the number of entries in the history.
Len() int
// At returns an entry from the history.
// Index 0 is the most-recently added entry and
// index Len()-1 is the least-recently added entry.
// If index is < 0 or >= Len(), it panics.
At(idx int) string
}
// Terminal contains the state for running a VT100 terminal that is capable of
// reading lines of input.
type Terminal struct {
@@ -44,6 +65,8 @@ type Terminal struct {
// bytes, as an index into |line|). If it returns ok=false, the key
// press is processed normally. Otherwise it returns a replacement line
// and the new cursor position.
//
// This will be disabled during ReadPassword.
AutoCompleteCallback func(line string, pos int, key rune) (newLine string, newPos int, ok bool)
// Escape contains a pointer to the escape codes for this terminal.
@@ -84,9 +107,14 @@ type Terminal struct {
remainder []byte
inBuf [256]byte
// history contains previously entered commands so that they can be
// accessed with the up and down keys.
history stRingBuffer
// History records and retrieves lines of input read by [ReadLine] which
// a user can retrieve and navigate using the up and down arrow keys.
//
// It is not safe to call ReadLine concurrently with any methods on History.
//
// [NewTerminal] sets this to a default implementation that records the
// last 100 lines of input.
History History
// historyIndex stores the currently accessed history entry, where zero
// means the immediately previous entry.
historyIndex int
@@ -109,6 +137,7 @@ func NewTerminal(c io.ReadWriter, prompt string) *Terminal {
termHeight: 24,
echo: true,
historyIndex: -1,
History: &stRingBuffer{},
}
}
@@ -117,6 +146,7 @@ const (
keyCtrlD = 4
keyCtrlU = 21
keyEnter = '\r'
keyLF = '\n'
keyEscape = 27
keyBackspace = 127
keyUnknown = 0xd800 /* UTF-16 surrogate area */ + iota
@@ -130,7 +160,9 @@ const (
keyEnd
keyDeleteWord
keyDeleteLine
keyDelete
keyClearScreen
keyTranspose
keyPasteStart
keyPasteEnd
)
@@ -164,6 +196,8 @@ func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
return keyDeleteLine, b[1:]
case 12: // ^L
return keyClearScreen, b[1:]
case 20: // ^T
return keyTranspose, b[1:]
case 23: // ^W
return keyDeleteWord, b[1:]
case 14: // ^N
@@ -198,6 +232,10 @@ func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
}
}
if !pasteActive && len(b) >= 4 && b[0] == keyEscape && b[1] == '[' && b[2] == '3' && b[3] == '~' {
return keyDelete, b[4:]
}
if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
switch b[5] {
case 'C':
@@ -383,7 +421,7 @@ func (t *Terminal) eraseNPreviousChars(n int) {
}
}
// countToLeftWord returns then number of characters from the cursor to the
// countToLeftWord returns the number of characters from the cursor to the
// start of the previous word.
func (t *Terminal) countToLeftWord() int {
if t.pos == 0 {
@@ -408,7 +446,7 @@ func (t *Terminal) countToLeftWord() int {
return t.pos - pos
}
// countToRightWord returns then number of characters from the cursor to the
// countToRightWord returns the number of characters from the cursor to the
// start of the next word.
func (t *Terminal) countToRightWord() int {
pos := t.pos
@@ -448,10 +486,27 @@ func visualLength(runes []rune) int {
return length
}
// historyAt unlocks the terminal and relocks it while calling History.At.
func (t *Terminal) historyAt(idx int) (string, bool) {
t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer.
defer t.lock.Lock() // panic in At (or Len) protection.
if idx < 0 || idx >= t.History.Len() {
return "", false
}
return t.History.At(idx), true
}
// historyAdd unlocks the terminal and relocks it while calling History.Add.
func (t *Terminal) historyAdd(entry string) {
t.lock.Unlock() // Unlock to avoid deadlock if History methods use the output writer.
defer t.lock.Lock() // panic in Add protection.
t.History.Add(entry)
}
// handleKey processes the given key and, optionally, returns a line of text
// that the user has entered.
func (t *Terminal) handleKey(key rune) (line string, ok bool) {
if t.pasteActive && key != keyEnter {
if t.pasteActive && key != keyEnter && key != keyLF {
t.addKeyToLine(key)
return
}
@@ -495,7 +550,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
t.pos = len(t.line)
t.moveCursorToPos(t.pos)
case keyUp:
entry, ok := t.history.NthPreviousEntry(t.historyIndex + 1)
entry, ok := t.historyAt(t.historyIndex + 1)
if !ok {
return "", false
}
@@ -514,14 +569,14 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
t.setLine(runes, len(runes))
t.historyIndex--
default:
entry, ok := t.history.NthPreviousEntry(t.historyIndex - 1)
entry, ok := t.historyAt(t.historyIndex - 1)
if ok {
t.historyIndex--
runes := []rune(entry)
t.setLine(runes, len(runes))
}
}
case keyEnter:
case keyEnter, keyLF:
t.moveCursorToPos(len(t.line))
t.queue([]rune("\r\n"))
line = string(t.line)
@@ -543,7 +598,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
}
t.line = t.line[:t.pos]
t.moveCursorToPos(t.pos)
case keyCtrlD:
case keyCtrlD, keyDelete:
// Erase the character under the current position.
// The EOF case when the line is empty is handled in
// readLine().
@@ -553,6 +608,24 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
}
case keyCtrlU:
t.eraseNPreviousChars(t.pos)
case keyTranspose:
// This transposes the two characters around the cursor and advances the cursor. Best-effort.
if len(t.line) < 2 || t.pos < 1 {
return
}
swap := t.pos
if swap == len(t.line) {
swap-- // special: at end of line, swap previous two chars
}
t.line[swap-1], t.line[swap] = t.line[swap], t.line[swap-1]
if t.pos < len(t.line) {
t.pos++
}
if t.echo {
t.moveCursorToPos(swap - 1)
t.writeLine(t.line[swap-1:])
t.moveCursorToPos(t.pos)
}
case keyClearScreen:
// Erases the screen and moves the cursor to the home position.
t.queue([]rune("\x1b[2J\x1b[H"))
@@ -692,6 +765,8 @@ func (t *Terminal) Write(buf []byte) (n int, err error) {
// ReadPassword temporarily changes the prompt and reads a password, without
// echo, from the terminal.
//
// The AutoCompleteCallback is disabled during this call.
func (t *Terminal) ReadPassword(prompt string) (line string, err error) {
t.lock.Lock()
defer t.lock.Unlock()
@@ -699,6 +774,11 @@ func (t *Terminal) ReadPassword(prompt string) (line string, err error) {
oldPrompt := t.prompt
t.prompt = []rune(prompt)
t.echo = false
oldAutoCompleteCallback := t.AutoCompleteCallback
t.AutoCompleteCallback = nil
defer func() {
t.AutoCompleteCallback = oldAutoCompleteCallback
}()
line, err = t.readLine()
@@ -759,6 +839,10 @@ func (t *Terminal) readLine() (line string, err error) {
if !t.pasteActive {
lineIsPasted = false
}
// If we have CR, consume LF if present (CRLF sequence) to avoid returning an extra empty line.
if key == keyEnter && len(rest) > 0 && rest[0] == keyLF {
rest = rest[1:]
}
line, lineOk = t.handleKey(key)
}
if len(rest) > 0 {
@@ -772,7 +856,7 @@ func (t *Terminal) readLine() (line string, err error) {
if lineOk {
if t.echo {
t.historyIndex = -1
t.history.Add(line)
t.historyAdd(line)
}
if lineIsPasted {
err = ErrPasteIndicator
@@ -929,19 +1013,23 @@ func (s *stRingBuffer) Add(a string) {
}
}
// NthPreviousEntry returns the value passed to the nth previous call to Add.
func (s *stRingBuffer) Len() int {
return s.size
}
// At returns the value passed to the nth previous call to Add.
// If n is zero then the immediately prior value is returned, if one, then the
// next most recent, and so on. If such an element doesn't exist then ok is
// false.
func (s *stRingBuffer) NthPreviousEntry(n int) (value string, ok bool) {
func (s *stRingBuffer) At(n int) string {
if n < 0 || n >= s.size {
return "", false
panic(fmt.Sprintf("term: history index [%d] out of range [0,%d)", n, s.size))
}
index := s.head - n
if index < 0 {
index += s.max
}
return s.entries[index], true
return s.entries[index]
}
// readPasswordLine reads from reader until it finds \n or io.EOF.

View File

@@ -6,6 +6,8 @@ package term
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"runtime"
@@ -208,12 +210,24 @@ var keyPressTests = []struct {
line: "efgh",
throwAwayLines: 1,
},
{
// Newline in bracketed paste mode should still work.
in: "abc\x1b[200~d\nefg\x1b[201~h\r",
line: "efgh",
throwAwayLines: 1,
},
{
// Lines consisting entirely of pasted data should be indicated as such.
in: "\x1b[200~a\r",
line: "a",
err: ErrPasteIndicator,
},
{
// Lines consisting entirely of pasted data should be indicated as such (\n paste).
in: "\x1b[200~a\n",
line: "a",
err: ErrPasteIndicator,
},
{
// Ctrl-C terminates readline
in: "\003",
@@ -224,6 +238,91 @@ var keyPressTests = []struct {
in: "a\003\r",
err: io.EOF,
},
{
// Delete at EOL: nothing
in: "abc\x1b[3~\r",
line: "abc",
},
{
// Delete in empty string: nothing
in: "\x1b[3~\r",
line: "",
},
{
// Move left, delete: delete 'c'
in: "abc\x1b[D\x1b[3~\r",
line: "ab",
},
{
// Home, delete: delete 'a'
in: "abc\x1b[H\x1b[3~\r",
line: "bc",
},
{
// Home, delete twice: delete 'a' and 'b'
in: "abc\x1b[H\x1b[3~\x1b[3~\r",
line: "c",
},
{
// Ctrl-T at end of line: transpose last two chars
in: "abc\x14\r",
line: "acb",
},
{
// Ctrl-T at end then type: cursor stays at end
in: "abc\x14N\r",
line: "acbN",
},
{
// Ctrl-T in middle: transpose chars before cursor, move cursor forward
in: "abc\x1b[D\x14\r",
line: "acb",
},
{
// Ctrl-T in middle then type: cursor moved past swapped char
in: "abcd\x1b[D\x1b[D\x14N\r",
line: "acbNd",
},
{
// Ctrl-T at pos 1 then type: cursor moves to pos 2
in: "abc\x1b[H\x1b[C\x14N\r",
line: "baNc",
},
{
// Ctrl-T with one char: do nothing
in: "a\x14\r",
line: "a",
},
{
// Ctrl-T with one char then type: cursor unchanged
in: "a\x14N\r",
line: "aN",
},
{
// Ctrl-T at beginning: do nothing
in: "ab\x1b[H\x14\r",
line: "ab",
},
{
// Ctrl-T at beginning then type: cursor unchanged, inserts at start
in: "ab\x1b[H\x14N\r",
line: "Nab",
},
{
// Ctrl-T on empty line: do nothing
in: "\x14\r",
line: "",
},
{
// Multiple Ctrl-T at end: keeps swapping last two
in: "abc\x14\x14\r",
line: "abc",
},
{
// Multiple Ctrl-T in middle: progresses through line
in: "abcd\x1b[D\x1b[D\x1b[D\x14\x14\x14\r",
line: "bcda",
},
}
func TestKeyPresses(t *testing.T) {
@@ -296,6 +395,36 @@ func TestRender(t *testing.T) {
}
}
func TestCRLF(t *testing.T) {
c := &MockTerminal{
toSend: []byte("line1\rline2\r\nline3\n"),
// bytesPerRead 0 in this test means read all at once
// CR+LF need to be in same read for ReadLine to not produce an extra empty line
// which is what terminals do for reasonably small paste. if way many lines are pasted
// and going over say 1k-16k buffer, readline current implementation will possibly generate 1
// extra empty line, if the CR is in chunk1 and LF in chunk2 (and that's fine).
}
ss := NewTerminal(c, "> ")
for i := range 3 {
line, err := ss.ReadLine()
if err != nil {
t.Fatalf("failed to read line %d: %v", i+1, err)
}
expected := fmt.Sprintf("line%d", i+1)
if line != expected {
t.Fatalf("expected '%s', got '%s'", expected, line)
}
}
line, err := ss.ReadLine()
if !errors.Is(err, io.EOF) {
t.Fatalf("expected EOF after 3 lines, got '%s' with error %v", line, err)
}
if line != "" {
t.Fatalf("expected empty line after EOF, got '%s'", line)
}
}
func TestPasswordNotSaved(t *testing.T) {
c := &MockTerminal{
toSend: []byte("password\r\x1b[A\r"),
@@ -343,7 +472,7 @@ func TestReadPasswordLineEnd(t *testing.T) {
input string
want string
}
var tests = []testType{
tests := []testType{
{"\r\n", ""},
{"test\r\n", "test"},
{"test\r", "test"},
@@ -396,6 +525,32 @@ func TestReadPasswordLineEnd(t *testing.T) {
}
}
func MockAutoCompleteCallback(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
return "not-good", pos, true
}
func TestReadPasswordDisabledAutoCompleteCallback(t *testing.T) {
input := "testgood\ranother line\r"
expectedPassword := "testgood"
terminal := NewTerminal(
&MockTerminal{
toSend: []byte(input),
bytesPerRead: 1,
},
"prompt")
terminal.AutoCompleteCallback = MockAutoCompleteCallback
password, err := terminal.ReadPassword("Password: ")
if err != nil {
t.Fatalf("failed to read password: %v", err)
}
if password != expectedPassword {
t.Fatalf("failed to read password, got %q", password)
}
if terminal.AutoCompleteCallback == nil {
t.Fatalf("AutoCompleteCallback should not be nil after ReadPassword")
}
}
func TestMakeRawState(t *testing.T) {
fd := int(os.Stdout.Fd())
if !IsTerminal(fd) {