Page Menu
Home
Musing Studio
Search
Configure Global Search
Log In
Files
F12571144
terminal.go
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
21 KB
Subscribers
None
terminal.go
View Options
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package
terminal
import
(
"bytes"
"io"
"sync"
"unicode/utf8"
)
// EscapeCodes contains escape sequences that can be written to the terminal in
// order to achieve different styles of text.
type
EscapeCodes
struct
{
// Foreground colors
Black
,
Red
,
Green
,
Yellow
,
Blue
,
Magenta
,
Cyan
,
White
[]
byte
// Reset all attributes
Reset
[]
byte
}
var
vt100EscapeCodes
=
EscapeCodes
{
Black
:
[]
byte
{
keyEscape
,
'['
,
'3'
,
'0'
,
'm'
},
Red
:
[]
byte
{
keyEscape
,
'['
,
'3'
,
'1'
,
'm'
},
Green
:
[]
byte
{
keyEscape
,
'['
,
'3'
,
'2'
,
'm'
},
Yellow
:
[]
byte
{
keyEscape
,
'['
,
'3'
,
'3'
,
'm'
},
Blue
:
[]
byte
{
keyEscape
,
'['
,
'3'
,
'4'
,
'm'
},
Magenta
:
[]
byte
{
keyEscape
,
'['
,
'3'
,
'5'
,
'm'
},
Cyan
:
[]
byte
{
keyEscape
,
'['
,
'3'
,
'6'
,
'm'
},
White
:
[]
byte
{
keyEscape
,
'['
,
'3'
,
'7'
,
'm'
},
Reset
:
[]
byte
{
keyEscape
,
'['
,
'0'
,
'm'
},
}
// Terminal contains the state for running a VT100 terminal that is capable of
// reading lines of input.
type
Terminal
struct
{
// AutoCompleteCallback, if non-null, is called for each keypress with
// the full input line and the current position of the cursor (in
// 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.
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.
// It's always a valid pointer, although the escape codes themselves
// may be empty if the terminal doesn't support them.
Escape
*
EscapeCodes
// lock protects the terminal and the state in this object from
// concurrent processing of a key press and a Write() call.
lock
sync
.
Mutex
c
io
.
ReadWriter
prompt
[]
rune
// line is the current line being entered.
line
[]
rune
// pos is the logical position of the cursor in line
pos
int
// echo is true if local echo is enabled
echo
bool
// pasteActive is true iff there is a bracketed paste operation in
// progress.
pasteActive
bool
// cursorX contains the current X value of the cursor where the left
// edge is 0. cursorY contains the row number where the first row of
// the current line is 0.
cursorX
,
cursorY
int
// maxLine is the greatest value of cursorY so far.
maxLine
int
termWidth
,
termHeight
int
// outBuf contains the terminal data to be sent.
outBuf
[]
byte
// remainder contains the remainder of any partial key sequences after
// a read. It aliases into inBuf.
remainder
[]
byte
inBuf
[
256
]
byte
// history contains previously entered commands so that they can be
// accessed with the up and down keys.
history
stRingBuffer
// historyIndex stores the currently accessed history entry, where zero
// means the immediately previous entry.
historyIndex
int
// When navigating up and down the history it's possible to return to
// the incomplete, initial line. That value is stored in
// historyPending.
historyPending
string
}
// NewTerminal runs a VT100 terminal on the given ReadWriter. If the ReadWriter is
// a local terminal, that terminal must first have been put into raw mode.
// prompt is a string that is written at the start of each input line (i.e.
// "> ").
func
NewTerminal
(
c
io
.
ReadWriter
,
prompt
string
)
*
Terminal
{
return
&
Terminal
{
Escape
:
&
vt100EscapeCodes
,
c
:
c
,
prompt
:
[]
rune
(
prompt
),
termWidth
:
80
,
termHeight
:
24
,
echo
:
true
,
historyIndex
:
-
1
,
}
}
const
(
keyCtrlD
=
4
keyCtrlU
=
21
keyEnter
=
'\r'
keyEscape
=
27
keyBackspace
=
127
keyUnknown
=
0xd800
/* UTF-16 surrogate area */
+
iota
keyUp
keyDown
keyLeft
keyRight
keyAltLeft
keyAltRight
keyHome
keyEnd
keyDeleteWord
keyDeleteLine
keyClearScreen
keyPasteStart
keyPasteEnd
)
var
(
crlf
=
[]
byte
{
'\r'
,
'\n'
}
pasteStart
=
[]
byte
{
keyEscape
,
'['
,
'2'
,
'0'
,
'0'
,
'~'
}
pasteEnd
=
[]
byte
{
keyEscape
,
'['
,
'2'
,
'0'
,
'1'
,
'~'
}
)
// bytesToKey tries to parse a key sequence from b. If successful, it returns
// the key and the remainder of the input. Otherwise it returns utf8.RuneError.
func
bytesToKey
(
b
[]
byte
,
pasteActive
bool
)
(
rune
,
[]
byte
)
{
if
len
(
b
)
==
0
{
return
utf8
.
RuneError
,
nil
}
if
!
pasteActive
{
switch
b
[
0
]
{
case
1
:
// ^A
return
keyHome
,
b
[
1
:]
case
5
:
// ^E
return
keyEnd
,
b
[
1
:]
case
8
:
// ^H
return
keyBackspace
,
b
[
1
:]
case
11
:
// ^K
return
keyDeleteLine
,
b
[
1
:]
case
12
:
// ^L
return
keyClearScreen
,
b
[
1
:]
case
23
:
// ^W
return
keyDeleteWord
,
b
[
1
:]
}
}
if
b
[
0
]
!=
keyEscape
{
if
!
utf8
.
FullRune
(
b
)
{
return
utf8
.
RuneError
,
b
}
r
,
l
:=
utf8
.
DecodeRune
(
b
)
return
r
,
b
[
l
:]
}
if
!
pasteActive
&&
len
(
b
)
>=
3
&&
b
[
0
]
==
keyEscape
&&
b
[
1
]
==
'['
{
switch
b
[
2
]
{
case
'A'
:
return
keyUp
,
b
[
3
:]
case
'B'
:
return
keyDown
,
b
[
3
:]
case
'C'
:
return
keyRight
,
b
[
3
:]
case
'D'
:
return
keyLeft
,
b
[
3
:]
case
'H'
:
return
keyHome
,
b
[
3
:]
case
'F'
:
return
keyEnd
,
b
[
3
:]
}
}
if
!
pasteActive
&&
len
(
b
)
>=
6
&&
b
[
0
]
==
keyEscape
&&
b
[
1
]
==
'['
&&
b
[
2
]
==
'1'
&&
b
[
3
]
==
';'
&&
b
[
4
]
==
'3'
{
switch
b
[
5
]
{
case
'C'
:
return
keyAltRight
,
b
[
6
:]
case
'D'
:
return
keyAltLeft
,
b
[
6
:]
}
}
if
!
pasteActive
&&
len
(
b
)
>=
6
&&
bytes
.
Equal
(
b
[:
6
],
pasteStart
)
{
return
keyPasteStart
,
b
[
6
:]
}
if
pasteActive
&&
len
(
b
)
>=
6
&&
bytes
.
Equal
(
b
[:
6
],
pasteEnd
)
{
return
keyPasteEnd
,
b
[
6
:]
}
// If we get here then we have a key that we don't recognise, or a
// partial sequence. It's not clear how one should find the end of a
// sequence without knowing them all, but it seems that [a-zA-Z~] only
// appears at the end of a sequence.
for
i
,
c
:=
range
b
[
0
:]
{
if
c
>=
'a'
&&
c
<=
'z'
||
c
>=
'A'
&&
c
<=
'Z'
||
c
==
'~'
{
return
keyUnknown
,
b
[
i
+
1
:]
}
}
return
utf8
.
RuneError
,
b
}
// queue appends data to the end of t.outBuf
func
(
t
*
Terminal
)
queue
(
data
[]
rune
)
{
t
.
outBuf
=
append
(
t
.
outBuf
,
[]
byte
(
string
(
data
))
...
)
}
var
eraseUnderCursor
=
[]
rune
{
' '
,
keyEscape
,
'['
,
'D'
}
var
space
=
[]
rune
{
' '
}
func
isPrintable
(
key
rune
)
bool
{
isInSurrogateArea
:=
key
>=
0xd800
&&
key
<=
0xdbff
return
key
>=
32
&&
!
isInSurrogateArea
}
// moveCursorToPos appends data to t.outBuf which will move the cursor to the
// given, logical position in the text.
func
(
t
*
Terminal
)
moveCursorToPos
(
pos
int
)
{
if
!
t
.
echo
{
return
}
x
:=
visualLength
(
t
.
prompt
)
+
pos
y
:=
x
/
t
.
termWidth
x
=
x
%
t
.
termWidth
up
:=
0
if
y
<
t
.
cursorY
{
up
=
t
.
cursorY
-
y
}
down
:=
0
if
y
>
t
.
cursorY
{
down
=
y
-
t
.
cursorY
}
left
:=
0
if
x
<
t
.
cursorX
{
left
=
t
.
cursorX
-
x
}
right
:=
0
if
x
>
t
.
cursorX
{
right
=
x
-
t
.
cursorX
}
t
.
cursorX
=
x
t
.
cursorY
=
y
t
.
move
(
up
,
down
,
left
,
right
)
}
func
(
t
*
Terminal
)
move
(
up
,
down
,
left
,
right
int
)
{
movement
:=
make
([]
rune
,
3
*
(
up
+
down
+
left
+
right
))
m
:=
movement
for
i
:=
0
;
i
<
up
;
i
++
{
m
[
0
]
=
keyEscape
m
[
1
]
=
'['
m
[
2
]
=
'A'
m
=
m
[
3
:]
}
for
i
:=
0
;
i
<
down
;
i
++
{
m
[
0
]
=
keyEscape
m
[
1
]
=
'['
m
[
2
]
=
'B'
m
=
m
[
3
:]
}
for
i
:=
0
;
i
<
left
;
i
++
{
m
[
0
]
=
keyEscape
m
[
1
]
=
'['
m
[
2
]
=
'D'
m
=
m
[
3
:]
}
for
i
:=
0
;
i
<
right
;
i
++
{
m
[
0
]
=
keyEscape
m
[
1
]
=
'['
m
[
2
]
=
'C'
m
=
m
[
3
:]
}
t
.
queue
(
movement
)
}
func
(
t
*
Terminal
)
clearLineToRight
()
{
op
:=
[]
rune
{
keyEscape
,
'['
,
'K'
}
t
.
queue
(
op
)
}
const
maxLineLength
=
4096
func
(
t
*
Terminal
)
setLine
(
newLine
[]
rune
,
newPos
int
)
{
if
t
.
echo
{
t
.
moveCursorToPos
(
0
)
t
.
writeLine
(
newLine
)
for
i
:=
len
(
newLine
);
i
<
len
(
t
.
line
);
i
++
{
t
.
writeLine
(
space
)
}
t
.
moveCursorToPos
(
newPos
)
}
t
.
line
=
newLine
t
.
pos
=
newPos
}
func
(
t
*
Terminal
)
advanceCursor
(
places
int
)
{
t
.
cursorX
+=
places
t
.
cursorY
+=
t
.
cursorX
/
t
.
termWidth
if
t
.
cursorY
>
t
.
maxLine
{
t
.
maxLine
=
t
.
cursorY
}
t
.
cursorX
=
t
.
cursorX
%
t
.
termWidth
if
places
>
0
&&
t
.
cursorX
==
0
{
// Normally terminals will advance the current position
// when writing a character. But that doesn't happen
// for the last character in a line. However, when
// writing a character (except a new line) that causes
// a line wrap, the position will be advanced two
// places.
//
// So, if we are stopping at the end of a line, we
// need to write a newline so that our cursor can be
// advanced to the next line.
t
.
outBuf
=
append
(
t
.
outBuf
,
'\r'
,
'\n'
)
}
}
func
(
t
*
Terminal
)
eraseNPreviousChars
(
n
int
)
{
if
n
==
0
{
return
}
if
t
.
pos
<
n
{
n
=
t
.
pos
}
t
.
pos
-=
n
t
.
moveCursorToPos
(
t
.
pos
)
copy
(
t
.
line
[
t
.
pos
:],
t
.
line
[
n
+
t
.
pos
:])
t
.
line
=
t
.
line
[:
len
(
t
.
line
)
-
n
]
if
t
.
echo
{
t
.
writeLine
(
t
.
line
[
t
.
pos
:])
for
i
:=
0
;
i
<
n
;
i
++
{
t
.
queue
(
space
)
}
t
.
advanceCursor
(
n
)
t
.
moveCursorToPos
(
t
.
pos
)
}
}
// countToLeftWord returns then number of characters from the cursor to the
// start of the previous word.
func
(
t
*
Terminal
)
countToLeftWord
()
int
{
if
t
.
pos
==
0
{
return
0
}
pos
:=
t
.
pos
-
1
for
pos
>
0
{
if
t
.
line
[
pos
]
!=
' '
{
break
}
pos
--
}
for
pos
>
0
{
if
t
.
line
[
pos
]
==
' '
{
pos
++
break
}
pos
--
}
return
t
.
pos
-
pos
}
// countToRightWord returns then number of characters from the cursor to the
// start of the next word.
func
(
t
*
Terminal
)
countToRightWord
()
int
{
pos
:=
t
.
pos
for
pos
<
len
(
t
.
line
)
{
if
t
.
line
[
pos
]
==
' '
{
break
}
pos
++
}
for
pos
<
len
(
t
.
line
)
{
if
t
.
line
[
pos
]
!=
' '
{
break
}
pos
++
}
return
pos
-
t
.
pos
}
// visualLength returns the number of visible glyphs in s.
func
visualLength
(
runes
[]
rune
)
int
{
inEscapeSeq
:=
false
length
:=
0
for
_
,
r
:=
range
runes
{
switch
{
case
inEscapeSeq
:
if
(
r
>=
'a'
&&
r
<=
'z'
)
||
(
r
>=
'A'
&&
r
<=
'Z'
)
{
inEscapeSeq
=
false
}
case
r
==
'\x1b'
:
inEscapeSeq
=
true
default
:
length
++
}
}
return
length
}
// 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
{
t
.
addKeyToLine
(
key
)
return
}
switch
key
{
case
keyBackspace
:
if
t
.
pos
==
0
{
return
}
t
.
eraseNPreviousChars
(
1
)
case
keyAltLeft
:
// move left by a word.
t
.
pos
-=
t
.
countToLeftWord
()
t
.
moveCursorToPos
(
t
.
pos
)
case
keyAltRight
:
// move right by a word.
t
.
pos
+=
t
.
countToRightWord
()
t
.
moveCursorToPos
(
t
.
pos
)
case
keyLeft
:
if
t
.
pos
==
0
{
return
}
t
.
pos
--
t
.
moveCursorToPos
(
t
.
pos
)
case
keyRight
:
if
t
.
pos
==
len
(
t
.
line
)
{
return
}
t
.
pos
++
t
.
moveCursorToPos
(
t
.
pos
)
case
keyHome
:
if
t
.
pos
==
0
{
return
}
t
.
pos
=
0
t
.
moveCursorToPos
(
t
.
pos
)
case
keyEnd
:
if
t
.
pos
==
len
(
t
.
line
)
{
return
}
t
.
pos
=
len
(
t
.
line
)
t
.
moveCursorToPos
(
t
.
pos
)
case
keyUp
:
entry
,
ok
:=
t
.
history
.
NthPreviousEntry
(
t
.
historyIndex
+
1
)
if
!
ok
{
return
""
,
false
}
if
t
.
historyIndex
==
-
1
{
t
.
historyPending
=
string
(
t
.
line
)
}
t
.
historyIndex
++
runes
:=
[]
rune
(
entry
)
t
.
setLine
(
runes
,
len
(
runes
))
case
keyDown
:
switch
t
.
historyIndex
{
case
-
1
:
return
case
0
:
runes
:=
[]
rune
(
t
.
historyPending
)
t
.
setLine
(
runes
,
len
(
runes
))
t
.
historyIndex
--
default
:
entry
,
ok
:=
t
.
history
.
NthPreviousEntry
(
t
.
historyIndex
-
1
)
if
ok
{
t
.
historyIndex
--
runes
:=
[]
rune
(
entry
)
t
.
setLine
(
runes
,
len
(
runes
))
}
}
case
keyEnter
:
t
.
moveCursorToPos
(
len
(
t
.
line
))
t
.
queue
([]
rune
(
"\r\n"
))
line
=
string
(
t
.
line
)
ok
=
true
t
.
line
=
t
.
line
[:
0
]
t
.
pos
=
0
t
.
cursorX
=
0
t
.
cursorY
=
0
t
.
maxLine
=
0
case
keyDeleteWord
:
// Delete zero or more spaces and then one or more characters.
t
.
eraseNPreviousChars
(
t
.
countToLeftWord
())
case
keyDeleteLine
:
// Delete everything from the current cursor position to the
// end of line.
for
i
:=
t
.
pos
;
i
<
len
(
t
.
line
);
i
++
{
t
.
queue
(
space
)
t
.
advanceCursor
(
1
)
}
t
.
line
=
t
.
line
[:
t
.
pos
]
t
.
moveCursorToPos
(
t
.
pos
)
case
keyCtrlD
:
// Erase the character under the current position.
// The EOF case when the line is empty is handled in
// readLine().
if
t
.
pos
<
len
(
t
.
line
)
{
t
.
pos
++
t
.
eraseNPreviousChars
(
1
)
}
case
keyCtrlU
:
t
.
eraseNPreviousChars
(
t
.
pos
)
case
keyClearScreen
:
// Erases the screen and moves the cursor to the home position.
t
.
queue
([]
rune
(
"\x1b[2J\x1b[H"
))
t
.
queue
(
t
.
prompt
)
t
.
cursorX
,
t
.
cursorY
=
0
,
0
t
.
advanceCursor
(
visualLength
(
t
.
prompt
))
t
.
setLine
(
t
.
line
,
t
.
pos
)
default
:
if
t
.
AutoCompleteCallback
!=
nil
{
prefix
:=
string
(
t
.
line
[:
t
.
pos
])
suffix
:=
string
(
t
.
line
[
t
.
pos
:])
t
.
lock
.
Unlock
()
newLine
,
newPos
,
completeOk
:=
t
.
AutoCompleteCallback
(
prefix
+
suffix
,
len
(
prefix
),
key
)
t
.
lock
.
Lock
()
if
completeOk
{
t
.
setLine
([]
rune
(
newLine
),
utf8
.
RuneCount
([]
byte
(
newLine
)[:
newPos
]))
return
}
}
if
!
isPrintable
(
key
)
{
return
}
if
len
(
t
.
line
)
==
maxLineLength
{
return
}
t
.
addKeyToLine
(
key
)
}
return
}
// addKeyToLine inserts the given key at the current position in the current
// line.
func
(
t
*
Terminal
)
addKeyToLine
(
key
rune
)
{
if
len
(
t
.
line
)
==
cap
(
t
.
line
)
{
newLine
:=
make
([]
rune
,
len
(
t
.
line
),
2
*
(
1
+
len
(
t
.
line
)))
copy
(
newLine
,
t
.
line
)
t
.
line
=
newLine
}
t
.
line
=
t
.
line
[:
len
(
t
.
line
)
+
1
]
copy
(
t
.
line
[
t
.
pos
+
1
:],
t
.
line
[
t
.
pos
:])
t
.
line
[
t
.
pos
]
=
key
if
t
.
echo
{
t
.
writeLine
(
t
.
line
[
t
.
pos
:])
}
t
.
pos
++
t
.
moveCursorToPos
(
t
.
pos
)
}
func
(
t
*
Terminal
)
writeLine
(
line
[]
rune
)
{
for
len
(
line
)
!=
0
{
remainingOnLine
:=
t
.
termWidth
-
t
.
cursorX
todo
:=
len
(
line
)
if
todo
>
remainingOnLine
{
todo
=
remainingOnLine
}
t
.
queue
(
line
[:
todo
])
t
.
advanceCursor
(
visualLength
(
line
[:
todo
]))
line
=
line
[
todo
:]
}
}
// writeWithCRLF writes buf to w but replaces all occurrences of \n with \r\n.
func
writeWithCRLF
(
w
io
.
Writer
,
buf
[]
byte
)
(
n
int
,
err
error
)
{
for
len
(
buf
)
>
0
{
i
:=
bytes
.
IndexByte
(
buf
,
'\n'
)
todo
:=
len
(
buf
)
if
i
>=
0
{
todo
=
i
}
var
nn
int
nn
,
err
=
w
.
Write
(
buf
[:
todo
])
n
+=
nn
if
err
!=
nil
{
return
n
,
err
}
buf
=
buf
[
todo
:]
if
i
>=
0
{
if
_
,
err
=
w
.
Write
(
crlf
);
err
!=
nil
{
return
n
,
err
}
n
++
buf
=
buf
[
1
:]
}
}
return
n
,
nil
}
func
(
t
*
Terminal
)
Write
(
buf
[]
byte
)
(
n
int
,
err
error
)
{
t
.
lock
.
Lock
()
defer
t
.
lock
.
Unlock
()
if
t
.
cursorX
==
0
&&
t
.
cursorY
==
0
{
// This is the easy case: there's nothing on the screen that we
// have to move out of the way.
return
writeWithCRLF
(
t
.
c
,
buf
)
}
// We have a prompt and possibly user input on the screen. We
// have to clear it first.
t
.
move
(
0
/* up */
,
0
/* down */
,
t
.
cursorX
/* left */
,
0
/* right */
)
t
.
cursorX
=
0
t
.
clearLineToRight
()
for
t
.
cursorY
>
0
{
t
.
move
(
1
/* up */
,
0
,
0
,
0
)
t
.
cursorY
--
t
.
clearLineToRight
()
}
if
_
,
err
=
t
.
c
.
Write
(
t
.
outBuf
);
err
!=
nil
{
return
}
t
.
outBuf
=
t
.
outBuf
[:
0
]
if
n
,
err
=
writeWithCRLF
(
t
.
c
,
buf
);
err
!=
nil
{
return
}
t
.
writeLine
(
t
.
prompt
)
if
t
.
echo
{
t
.
writeLine
(
t
.
line
)
}
t
.
moveCursorToPos
(
t
.
pos
)
if
_
,
err
=
t
.
c
.
Write
(
t
.
outBuf
);
err
!=
nil
{
return
}
t
.
outBuf
=
t
.
outBuf
[:
0
]
return
}
// ReadPassword temporarily changes the prompt and reads a password, without
// echo, from the terminal.
func
(
t
*
Terminal
)
ReadPassword
(
prompt
string
)
(
line
string
,
err
error
)
{
t
.
lock
.
Lock
()
defer
t
.
lock
.
Unlock
()
oldPrompt
:=
t
.
prompt
t
.
prompt
=
[]
rune
(
prompt
)
t
.
echo
=
false
line
,
err
=
t
.
readLine
()
t
.
prompt
=
oldPrompt
t
.
echo
=
true
return
}
// ReadLine returns a line of input from the terminal.
func
(
t
*
Terminal
)
ReadLine
()
(
line
string
,
err
error
)
{
t
.
lock
.
Lock
()
defer
t
.
lock
.
Unlock
()
return
t
.
readLine
()
}
func
(
t
*
Terminal
)
readLine
()
(
line
string
,
err
error
)
{
// t.lock must be held at this point
if
t
.
cursorX
==
0
&&
t
.
cursorY
==
0
{
t
.
writeLine
(
t
.
prompt
)
t
.
c
.
Write
(
t
.
outBuf
)
t
.
outBuf
=
t
.
outBuf
[:
0
]
}
lineIsPasted
:=
t
.
pasteActive
for
{
rest
:=
t
.
remainder
lineOk
:=
false
for
!
lineOk
{
var
key
rune
key
,
rest
=
bytesToKey
(
rest
,
t
.
pasteActive
)
if
key
==
utf8
.
RuneError
{
break
}
if
!
t
.
pasteActive
{
if
key
==
keyCtrlD
{
if
len
(
t
.
line
)
==
0
{
return
""
,
io
.
EOF
}
}
if
key
==
keyPasteStart
{
t
.
pasteActive
=
true
if
len
(
t
.
line
)
==
0
{
lineIsPasted
=
true
}
continue
}
}
else
if
key
==
keyPasteEnd
{
t
.
pasteActive
=
false
continue
}
if
!
t
.
pasteActive
{
lineIsPasted
=
false
}
line
,
lineOk
=
t
.
handleKey
(
key
)
}
if
len
(
rest
)
>
0
{
n
:=
copy
(
t
.
inBuf
[:],
rest
)
t
.
remainder
=
t
.
inBuf
[:
n
]
}
else
{
t
.
remainder
=
nil
}
t
.
c
.
Write
(
t
.
outBuf
)
t
.
outBuf
=
t
.
outBuf
[:
0
]
if
lineOk
{
if
t
.
echo
{
t
.
historyIndex
=
-
1
t
.
history
.
Add
(
line
)
}
if
lineIsPasted
{
err
=
ErrPasteIndicator
}
return
}
// t.remainder is a slice at the beginning of t.inBuf
// containing a partial key sequence
readBuf
:=
t
.
inBuf
[
len
(
t
.
remainder
):]
var
n
int
t
.
lock
.
Unlock
()
n
,
err
=
t
.
c
.
Read
(
readBuf
)
t
.
lock
.
Lock
()
if
err
!=
nil
{
return
}
t
.
remainder
=
t
.
inBuf
[:
n
+
len
(
t
.
remainder
)]
}
}
// SetPrompt sets the prompt to be used when reading subsequent lines.
func
(
t
*
Terminal
)
SetPrompt
(
prompt
string
)
{
t
.
lock
.
Lock
()
defer
t
.
lock
.
Unlock
()
t
.
prompt
=
[]
rune
(
prompt
)
}
func
(
t
*
Terminal
)
clearAndRepaintLinePlusNPrevious
(
numPrevLines
int
)
{
// Move cursor to column zero at the start of the line.
t
.
move
(
t
.
cursorY
,
0
,
t
.
cursorX
,
0
)
t
.
cursorX
,
t
.
cursorY
=
0
,
0
t
.
clearLineToRight
()
for
t
.
cursorY
<
numPrevLines
{
// Move down a line
t
.
move
(
0
,
1
,
0
,
0
)
t
.
cursorY
++
t
.
clearLineToRight
()
}
// Move back to beginning.
t
.
move
(
t
.
cursorY
,
0
,
0
,
0
)
t
.
cursorX
,
t
.
cursorY
=
0
,
0
t
.
queue
(
t
.
prompt
)
t
.
advanceCursor
(
visualLength
(
t
.
prompt
))
t
.
writeLine
(
t
.
line
)
t
.
moveCursorToPos
(
t
.
pos
)
}
func
(
t
*
Terminal
)
SetSize
(
width
,
height
int
)
error
{
t
.
lock
.
Lock
()
defer
t
.
lock
.
Unlock
()
if
width
==
0
{
width
=
1
}
oldWidth
:=
t
.
termWidth
t
.
termWidth
,
t
.
termHeight
=
width
,
height
switch
{
case
width
==
oldWidth
:
// If the width didn't change then nothing else needs to be
// done.
return
nil
case
len
(
t
.
line
)
==
0
&&
t
.
cursorX
==
0
&&
t
.
cursorY
==
0
:
// If there is nothing on current line and no prompt printed,
// just do nothing
return
nil
case
width
<
oldWidth
:
// Some terminals (e.g. xterm) will truncate lines that were
// too long when shinking. Others, (e.g. gnome-terminal) will
// attempt to wrap them. For the former, repainting t.maxLine
// works great, but that behaviour goes badly wrong in the case
// of the latter because they have doubled every full line.
// We assume that we are working on a terminal that wraps lines
// and adjust the cursor position based on every previous line
// wrapping and turning into two. This causes the prompt on
// xterms to move upwards, which isn't great, but it avoids a
// huge mess with gnome-terminal.
if
t
.
cursorX
>=
t
.
termWidth
{
t
.
cursorX
=
t
.
termWidth
-
1
}
t
.
cursorY
*=
2
t
.
clearAndRepaintLinePlusNPrevious
(
t
.
maxLine
*
2
)
case
width
>
oldWidth
:
// If the terminal expands then our position calculations will
// be wrong in the future because we think the cursor is
// |t.pos| chars into the string, but there will be a gap at
// the end of any wrapped line.
//
// But the position will actually be correct until we move, so
// we can move back to the beginning and repaint everything.
t
.
clearAndRepaintLinePlusNPrevious
(
t
.
maxLine
)
}
_
,
err
:=
t
.
c
.
Write
(
t
.
outBuf
)
t
.
outBuf
=
t
.
outBuf
[:
0
]
return
err
}
type
pasteIndicatorError
struct
{}
func
(
pasteIndicatorError
)
Error
()
string
{
return
"terminal: ErrPasteIndicator not correctly handled"
}
// ErrPasteIndicator may be returned from ReadLine as the error, in addition
// to valid line data. It indicates that bracketed paste mode is enabled and
// that the returned line consists only of pasted data. Programs may wish to
// interpret pasted data more literally than typed data.
var
ErrPasteIndicator
=
pasteIndicatorError
{}
// SetBracketedPasteMode requests that the terminal bracket paste operations
// with markers. Not all terminals support this but, if it is supported, then
// enabling this mode will stop any autocomplete callback from running due to
// pastes. Additionally, any lines that are completely pasted will be returned
// from ReadLine with the error set to ErrPasteIndicator.
func
(
t
*
Terminal
)
SetBracketedPasteMode
(
on
bool
)
{
if
on
{
io
.
WriteString
(
t
.
c
,
"\x1b[?2004h"
)
}
else
{
io
.
WriteString
(
t
.
c
,
"\x1b[?2004l"
)
}
}
// stRingBuffer is a ring buffer of strings.
type
stRingBuffer
struct
{
// entries contains max elements.
entries
[]
string
max
int
// head contains the index of the element most recently added to the ring.
head
int
// size contains the number of elements in the ring.
size
int
}
func
(
s
*
stRingBuffer
)
Add
(
a
string
)
{
if
s
.
entries
==
nil
{
const
defaultNumEntries
=
100
s
.
entries
=
make
([]
string
,
defaultNumEntries
)
s
.
max
=
defaultNumEntries
}
s
.
head
=
(
s
.
head
+
1
)
%
s
.
max
s
.
entries
[
s
.
head
]
=
a
if
s
.
size
<
s
.
max
{
s
.
size
++
}
}
// NthPreviousEntry 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
)
{
if
n
>=
s
.
size
{
return
""
,
false
}
index
:=
s
.
head
-
n
if
index
<
0
{
index
+=
s
.
max
}
return
s
.
entries
[
index
],
true
}
// readPasswordLine reads from reader until it finds \n or io.EOF.
// The slice returned does not include the \n.
// readPasswordLine also ignores any \r it finds.
func
readPasswordLine
(
reader
io
.
Reader
)
([]
byte
,
error
)
{
var
buf
[
1
]
byte
var
ret
[]
byte
for
{
n
,
err
:=
reader
.
Read
(
buf
[:])
if
n
>
0
{
switch
buf
[
0
]
{
case
'\n'
:
return
ret
,
nil
case
'\r'
:
// remove \r from passwords on Windows
default
:
ret
=
append
(
ret
,
buf
[
0
])
}
continue
}
if
err
!=
nil
{
if
err
==
io
.
EOF
&&
len
(
ret
)
>
0
{
return
ret
,
nil
}
return
ret
,
err
}
}
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Nov 23, 8:38 AM (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3502280
Attached To
rWCLI writeas-cli
Event Timeline
Log In to Comment