Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
K
koko
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ops
koko
Commits
7d67f134
Commit
7d67f134
authored
Apr 29, 2019
by
ibuler
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[Update] 修改命令解析
parent
870b9366
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
1119 additions
and
20 deletions
+1119
-20
hello.go
cmd/hello.go
+1
-0
parser.go
pkg/proxy/parser.go
+75
-9
proxy.go
pkg/proxy/proxy.go
+5
-11
terminal.go
pkg/utils/terminal.go
+1038
-0
No files found.
cmd/hello.go
0 → 100644
View file @
7d67f134
package
main
pkg/proxy/parser.go
View file @
7d67f134
...
@@ -3,10 +3,13 @@ package proxy
...
@@ -3,10 +3,13 @@ package proxy
import
(
import
(
"bytes"
"bytes"
"fmt"
"fmt"
"os"
"strings"
"sync"
"sync"
"cocogo/pkg/logger"
"cocogo/pkg/logger"
"cocogo/pkg/model"
"cocogo/pkg/model"
"cocogo/pkg/utils"
)
)
type
ParseRule
func
([]
byte
)
bool
type
ParseRule
func
([]
byte
)
bool
...
@@ -26,15 +29,34 @@ var (
...
@@ -26,15 +29,34 @@ var (
charEnter
=
[]
byte
(
"
\r
"
)
charEnter
=
[]
byte
(
"
\r
"
)
)
)
type
CmdParser
struct
{
term
*
utils
.
Terminal
buf
*
bytes
.
Buffer
}
func
(
cp
*
CmdParser
)
Reset
()
{
cp
.
buf
.
Reset
()
}
func
(
cp
*
CmdParser
)
Initial
()
{
cp
.
buf
=
new
(
bytes
.
Buffer
)
cp
.
term
=
utils
.
NewTerminal
(
cp
.
buf
,
""
)
cp
.
term
.
SetEcho
(
false
)
}
func
(
cp
*
CmdParser
)
Parse
(
b
[]
byte
)
string
{
cp
.
buf
.
Write
(
b
)
cp
.
buf
.
WriteString
(
"
\r
"
)
lines
,
_
:=
cp
.
term
.
ReadLines
()
return
strings
.
TrimSpace
(
strings
.
Join
(
lines
,
"
\r\n
"
))
}
// Parse 解析用户输入输出, 拦截过滤用户输入输出
// Parse 解析用户输入输出, 拦截过滤用户输入输出
type
Parser
struct
{
type
Parser
struct
{
inputBuf
*
bytes
.
Buffer
inputBuf
*
bytes
.
Buffer
cmdBuf
*
bytes
.
Buffer
cmdBuf
*
bytes
.
Buffer
outputBuf
*
bytes
.
Buffer
outputBuf
*
bytes
.
Buffer
userInputChan
chan
[]
byte
serverInputChan
chan
[]
byte
filterRules
[]
model
.
SystemUserFilterRule
filterRules
[]
model
.
SystemUserFilterRule
inputInitial
bool
inputInitial
bool
...
@@ -44,6 +66,25 @@ type Parser struct {
...
@@ -44,6 +66,25 @@ type Parser struct {
zmodemState
string
zmodemState
string
inVimState
bool
inVimState
bool
once
sync
.
Once
once
sync
.
Once
command
string
output
string
cmdInputParser
*
CmdParser
cmdOutputParser
*
CmdParser
counter
int
}
func
(
p
*
Parser
)
Initial
()
{
p
.
inputBuf
=
new
(
bytes
.
Buffer
)
p
.
cmdBuf
=
new
(
bytes
.
Buffer
)
p
.
outputBuf
=
new
(
bytes
.
Buffer
)
p
.
once
=
sync
.
Once
{}
p
.
cmdInputParser
=
&
CmdParser
{}
p
.
cmdOutputParser
=
&
CmdParser
{}
p
.
cmdInputParser
.
Initial
()
p
.
cmdOutputParser
.
Initial
()
}
}
// Todo: parseMultipleInput 依然存在问题
// Todo: parseMultipleInput 依然存在问题
...
@@ -56,17 +97,41 @@ func (p *Parser) parseInputState(b []byte) {
...
@@ -56,17 +97,41 @@ func (p *Parser) parseInputState(b []byte) {
p
.
inputPreState
=
p
.
inputState
p
.
inputPreState
=
p
.
inputState
if
bytes
.
Contains
(
b
,
charEnter
)
{
if
bytes
.
Contains
(
b
,
charEnter
)
{
p
.
inputState
=
false
p
.
inputState
=
false
//fmt.Printf("Command: %s\n", p.inputBuf.String())
p
.
parseCmdInput
()
p
.
inputBuf
.
Reset
()
}
else
{
}
else
{
p
.
inputState
=
true
p
.
inputState
=
true
if
!
p
.
inputPreState
{
if
!
p
.
inputPreState
{
//fmt.Printf("Output: %s\n", p.outputBuf.String())
p
.
parseCmdOutput
()
p
.
outputBuf
.
Reset
()
}
}
}
}
}
}
var
f
,
_
=
os
.
Create
(
"/tmp/cmd.text"
)
func
(
p
*
Parser
)
parseCmdInput
()
{
parser
:=
CmdParser
{}
parser
.
Initial
()
data
:=
p
.
cmdBuf
.
Bytes
()
fmt
.
Printf
(
"原始输入: %b
\n
"
,
data
)
line
:=
parser
.
Parse
(
data
)
data2
:=
fmt
.
Sprintf
(
"[%d] 命令: %s
\n
"
,
p
.
counter
,
line
)
fmt
.
Printf
(
data2
)
p
.
cmdBuf
.
Reset
()
p
.
inputBuf
.
Reset
()
f
.
WriteString
(
data2
)
}
func
(
p
*
Parser
)
parseCmdOutput
()
{
data
:=
p
.
outputBuf
.
Bytes
()
line
:=
p
.
cmdOutputParser
.
Parse
(
data
)
data2
:=
fmt
.
Sprintf
(
"[%d] 结果: %s
\n
"
,
p
.
counter
,
line
)
_
=
fmt
.
Sprintf
(
"[%d] 结果: %s
\n
"
,
p
.
counter
,
line
)
fmt
.
Printf
(
data2
)
p
.
outputBuf
.
Reset
()
f
.
WriteString
(
data2
)
p
.
counter
+=
1
}
func
(
p
*
Parser
)
parseInputNewLine
(
b
[]
byte
)
[]
byte
{
func
(
p
*
Parser
)
parseInputNewLine
(
b
[]
byte
)
[]
byte
{
b
=
bytes
.
Replace
(
b
,
[]
byte
{
'\r'
,
'\r'
,
'\n'
},
[]
byte
{
'\r'
},
-
1
)
b
=
bytes
.
Replace
(
b
,
[]
byte
{
'\r'
,
'\r'
,
'\n'
},
[]
byte
{
'\r'
},
-
1
)
b
=
bytes
.
Replace
(
b
,
[]
byte
{
'\r'
,
'\n'
},
[]
byte
{
'\r'
},
-
1
)
b
=
bytes
.
Replace
(
b
,
[]
byte
{
'\r'
,
'\n'
},
[]
byte
{
'\r'
},
-
1
)
...
@@ -80,7 +145,6 @@ func (p *Parser) ParseUserInput(b []byte) []byte {
...
@@ -80,7 +145,6 @@ func (p *Parser) ParseUserInput(b []byte) []byte {
})
})
nb
:=
p
.
parseInputNewLine
(
b
)
nb
:=
p
.
parseInputNewLine
(
b
)
p
.
inputBuf
.
Write
(
nb
)
p
.
inputBuf
.
Write
(
nb
)
fmt
.
Printf
(
"User input: %b
\n
"
,
b
)
p
.
parseInputState
(
nb
)
p
.
parseInputState
(
nb
)
return
b
return
b
}
}
...
@@ -117,6 +181,9 @@ func (p *Parser) parseZmodemState(b []byte) {
...
@@ -117,6 +181,9 @@ func (p *Parser) parseZmodemState(b []byte) {
}
}
func
(
p
*
Parser
)
parseCommand
(
b
[]
byte
)
{
func
(
p
*
Parser
)
parseCommand
(
b
[]
byte
)
{
if
!
p
.
inputInitial
{
return
}
if
p
.
inputState
{
if
p
.
inputState
{
p
.
cmdBuf
.
Write
(
b
)
p
.
cmdBuf
.
Write
(
b
)
}
else
{
}
else
{
...
@@ -128,6 +195,5 @@ func (p *Parser) ParseServerOutput(b []byte) []byte {
...
@@ -128,6 +195,5 @@ func (p *Parser) ParseServerOutput(b []byte) []byte {
p
.
parseVimState
(
b
)
p
.
parseVimState
(
b
)
p
.
parseZmodemState
(
b
)
p
.
parseZmodemState
(
b
)
p
.
parseCommand
(
b
)
p
.
parseCommand
(
b
)
fmt
.
Printf
(
"Server output: %s
\n
"
,
b
)
return
b
return
b
}
}
pkg/proxy/proxy.go
View file @
7d67f134
package
proxy
package
proxy
import
(
import
(
"bytes"
"context"
"context"
"sync"
"github.com/ibuler/ssh"
"github.com/ibuler/ssh"
"cocogo/pkg/logger"
"cocogo/pkg/logger"
...
@@ -72,17 +69,14 @@ func (p *ProxyServer) Proxy(ctx context.Context) {
...
@@ -72,17 +69,14 @@ func (p *ProxyServer) Proxy(ctx context.Context) {
if
err
!=
nil
{
if
err
!=
nil
{
logger
.
Error
(
"Get system user filter rule error: "
,
err
)
logger
.
Error
(
"Get system user filter rule error: "
,
err
)
}
}
parser
:=
&
Parser
{
filterRules
:
rules
,
}
parser
.
Initial
()
sw
:=
Switch
{
sw
:=
Switch
{
userSession
:
p
.
Session
,
userSession
:
p
.
Session
,
serverConn
:
&
conn
,
serverConn
:
&
conn
,
parser
:
&
Parser
{
parser
:
parser
,
once
:
sync
.
Once
{},
userInputChan
:
make
(
chan
[]
byte
,
5
),
inputBuf
:
new
(
bytes
.
Buffer
),
outputBuf
:
new
(
bytes
.
Buffer
),
cmdBuf
:
new
(
bytes
.
Buffer
),
filterRules
:
rules
,
},
}
}
sw
.
Bridge
(
ctx
)
sw
.
Bridge
(
ctx
)
}
}
pkg/utils/terminal.go
0 → 100644
View file @
7d67f134
// 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
utils
import
(
"bytes"
"fmt"
"io"
"strconv"
"sync"
"time"
"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
:
]
case
14
:
// ^N
return
keyDown
,
b
[
1
:
]
case
16
:
// ^P
return
keyUp
,
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
)
{
m
:=
[]
rune
{}
// 1 unit up can be expressed as ^[[A or ^[A
// 5 units up can be expressed as ^[[5A
if
up
==
1
{
m
=
append
(
m
,
keyEscape
,
'['
,
'A'
)
}
else
if
up
>
1
{
m
=
append
(
m
,
keyEscape
,
'['
)
m
=
append
(
m
,
[]
rune
(
strconv
.
Itoa
(
up
))
...
)
m
=
append
(
m
,
'A'
)
}
if
down
==
1
{
m
=
append
(
m
,
keyEscape
,
'['
,
'B'
)
}
else
if
down
>
1
{
m
=
append
(
m
,
keyEscape
,
'['
)
m
=
append
(
m
,
[]
rune
(
strconv
.
Itoa
(
down
))
...
)
m
=
append
(
m
,
'B'
)
}
if
right
==
1
{
m
=
append
(
m
,
keyEscape
,
'['
,
'C'
)
}
else
if
right
>
1
{
m
=
append
(
m
,
keyEscape
,
'['
)
m
=
append
(
m
,
[]
rune
(
strconv
.
Itoa
(
right
))
...
)
m
=
append
(
m
,
'C'
)
}
if
left
==
1
{
m
=
append
(
m
,
keyEscape
,
'['
,
'D'
)
}
else
if
left
>
1
{
m
=
append
(
m
,
keyEscape
,
'['
)
m
=
append
(
m
,
[]
rune
(
strconv
.
Itoa
(
left
))
...
)
m
=
append
(
m
,
'D'
)
}
t
.
queue
(
m
)
}
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
)
SetEcho
(
echo
bool
)
{
t
.
echo
=
echo
}
func
(
t
*
Terminal
)
ReadLines
()
(
lines
[]
string
,
err
error
)
{
t
.
lock
.
Lock
()
defer
t
.
lock
.
Unlock
()
var
line
string
if
t
.
cursorX
==
0
&&
t
.
cursorY
==
0
{
t
.
writeLine
(
t
.
prompt
)
t
.
c
.
Write
(
t
.
outBuf
)
t
.
outBuf
=
t
.
outBuf
[
:
0
]
}
for
{
rest
:=
t
.
remainder
lineOk
:=
false
for
len
(
rest
)
>
0
{
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
lines
,
io
.
EOF
}
}
if
key
==
keyPasteStart
{
t
.
pasteActive
=
true
continue
}
}
else
if
key
==
keyPasteEnd
{
t
.
pasteActive
=
false
continue
}
line
,
lineOk
=
t
.
handleKey
(
key
)
if
lineOk
{
lines
=
append
(
lines
,
line
)
}
}
if
len
(
rest
)
>
0
{
n
:=
copy
(
t
.
inBuf
[
:
],
rest
)
t
.
remainder
=
t
.
inBuf
[
:
n
]
}
else
{
t
.
remainder
=
nil
}
// 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
)]
}
}
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
fmt
.
Printf
(
"Before: Rest %d
\n
"
,
len
(
rest
))
key
,
rest
=
bytesToKey
(
rest
,
t
.
pasteActive
)
fmt
.
Printf
(
"After: Key: %v Rest %d
\n
"
,
key
,
len
(
rest
))
time
.
Sleep
(
10
*
time
.
Millisecond
)
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
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment