Compare commits

...

29 Commits

Author SHA1 Message Date
re 295ca315c5 fix repos 2022-12-12 16:55:00 +03:00
ChenYe 7f93d88cd5 fixing deadloop when input is filtered 2022-07-15 20:48:48 +08:00
Thomas O'Dowd 8e4bd417b9
Handle keypad mode cursor key escape sequences. (#203)
Normally the terminal uses CSI escape sequences when the UP, DOWN,
LEFT, RIGHT and HOME, END keys are pressed. These look like the
following ESC [ A etc, where ESC [ is the CSI sequence.

xterm and other terminals however can generate an alternative
escape sequence called SS3 if in the application keypad mode.
This sequence is ESC O A etc.

Bash readline understands both modes so nowadays you rarely
see OA being printed when you press the up arrow while the terminal
is using the keypad mode. readline currently does not understand
these sequences.

To test this fix, I used an xterm and put it in keypad mode
using the command "tput smkx". Then I started the readline-demo
and tried using arrow keys. Without this fix, OA is printed when
I press up. With this fix, readline fetches the previous command
as per regular mode. After testing you can escape back to
regular mode using "tput rmkx".
2022-05-20 21:29:21 +08:00
ChenYe 80e2d1961b
restore term when receive signal (#200)
* restore term when receive signal

add `CaptureExitSignal` to capture exit signals and exit gracefull(disabled by default)

* update deps
2022-04-24 21:25:55 +08:00
Derick Rethans c34436b973
Added 'os400' (#201)
As per https://github.com/chzyer/readline/pull/175#issuecomment-1100887727
2022-04-18 20:41:19 +08:00
Clément Chigot a5e9f81cc2
AIX support (#175)
This commit adds support for AIX operating system.

 - move term_solaris.go to term_nosyscall6.go. AIX like solaris doesn't
provide syscall.Syscall6 and must rely on x/sys/unix in order to perform
syscalls.

 - This patch won't work with versions prior to 1.13 because it needs
some constants added by https://go-review.googlesource.com/c/go/+/171339.
2022-04-17 16:50:25 +08:00
ChenYe a11d8f26cf add go.mod and remove unnecessary dep 2022-04-13 22:35:28 +08:00
Jim-wiselike 2972be24d4 Modify the display width for Chinese characters and so on (#145)
The display width will not be right if there have Chinese characters in the display. It will overflow the line, if the Chinese characters are at the end of a line. And makes "lines" value not the right number of line rows,  which makes "239: fmt.Fprintf" produce wrong output.
2018-06-03 21:26:55 +08:00
soopsio f6d7a1f6fb Fix ioloop groutine leaks bug. (#136)
* Fix ioloop groutine leaks bug.

* fix func (o *Operation) ioloop()  L:125 hangs
2017-12-08 09:17:16 +08:00
François-Xavier Aguessy 40d6036c33 Fix panic 'integer divide by zero' when terminal width too low (#137) 2017-12-08 09:16:05 +08:00
Abiola Ibrahim a4d5111b61 Fix panic for ReadPassword (#133) 2017-11-03 21:19:23 +08:00
Eugene Apollonsky 6a4bc7b4fe operation: fix SetConfig races (#131) 2017-10-03 22:59:50 +08:00
mrsinham 9cc74fe5ad Prefill user input (#101)
* Using a fillable stdin reader

* Adding some documentation to the new features
2017-10-02 20:43:15 +08:00
mrsinham af545c8af6 enabling disabling history (#102) 2017-10-02 20:42:28 +08:00
mrsinham aee0fa669f fix: on linux multine move was not working (#103)
on linux, if you want to change line, \b are not enough. You need to change line
and move to the right.
2017-10-02 20:39:53 +08:00
Saeed Rasooli aa9ed7db49 NewEx: fix panic if cfg.Painter is not given (#129) 2017-10-02 14:46:29 +08:00
Jelmer Snoeck c6c3e8d906 Add support for bold text. (#106)
Currently, bold text isn't supported. This adds the support for bold
text formatting.
2017-10-01 22:59:06 -05:00
Thomas Bradford 707fd8ecaa intercept line rendering using a 'Painter' (#116) 2017-10-01 22:58:28 -05:00
Sagar Mittal 9f56defe66 Add barebones support for yank/paste (#120) 2017-10-01 22:57:50 -05:00
Edward Betts 045fff973b correct spelling mistake (#127) 2017-10-01 22:57:10 -05:00
Davor Kapsa 8d510b4136 travis: update go version (#128) 2017-10-01 22:55:11 -05:00
Jordan Lewis 01c4e90c35 Bugfix in rewriteHistory (#121)
Previously, rewriteHistory was incorrect - it did not preserve the
newlines between history lines, which corrupts the history file.
2017-10-01 22:53:35 -05:00
Paul Tagliamonte 41eea22f71 Add two API methods to set RuneBuffer state before prompting the user (#105)
* Expose RuneBuffer.Set through Operation's API

This allows a user to prefill a line before the user is given a prompt,
so that you can provide a default, user-editable input line.

* Add a Instance.ReadlineWithDefault API method

This will pre-fill the Operation.RuneBuffer with text before prompting
the user, and provide her with a method to edit the provided default.
2017-03-14 07:49:21 +08:00
Gereon Frey 8a1389155f Solaris support (#110)
* Add support for solaris

* Change state to handle system dependent termios type.
* Move syscalls to get and set termios to functions in files only built
  for respective platforms.
* Create `term_unix.go` go file built on all supported unices except
  solaris with types and functions valid for all of them.
* Change `MakeRaw` to set VMIN and VTIME to default values.

Fixes #95.

* Fix error handling

Doing the string comparison could be improved, but at least we should
return an error, if it is not "errno 0".
2017-03-13 07:57:45 +08:00
Kenneth Shaw 784bd70fe0 Adding knq/usql to README.md (#107)
usql is a new project that is making extensive use of readline. usql is
roughly 3 days old, and already has over 1100 stars. This change adds
usql to the README.md and reorders projects listed in the "Repos using
readline" section.
2017-03-05 19:31:14 +08:00
Jiří Setnička f892600c35 Ability to filter out input runes (#104)
Could be used to mask/disable some actions (like Ctrl-Z, or searching by
Ctrl-R and Ctrl-S)
2017-02-20 09:51:01 +08:00
chzyer eef24db1d5 fix terminal data race (#99) 2017-01-25 11:55:32 +08:00
Remi Reuvekamp c914be64f0 Fix panic: runtime error: slice bounds out of range (#94)
Reslicing line[10:] went wrong if only 'setprompt' was entered, without a space after.
2016-11-06 12:23:43 +08:00
chzyer 25c2772d5f fix deadlock in cancelable stdin (#89)
* fix deadlock in cancelable stdin

* fix notify back

* [cancelable stdin] fix c.notify
2016-10-06 12:35:07 +08:00
31 changed files with 584 additions and 145 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.vscode/*

View File

@ -1,9 +1,8 @@
language: go language: go
go: go:
- 1.5 - 1.x
- 1.7
script: script:
- GOOS=windows go install github.com/chzyer/readline/example/... - GOOS=windows go install git.internal/re/readline/example/...
- GOOS=linux go install github.com/chzyer/readline/example/... - GOOS=linux go install git.internal/re/readline/example/...
- GOOS=darwin go install github.com/chzyer/readline/example/... - GOOS=darwin go install git.internal/re/readline/example/...
- go test -race -v - go test -race -v

View File

@ -11,7 +11,7 @@
* [#38][38] add SetChildren for prefix completer interface * [#38][38] add SetChildren for prefix completer interface
* [#42][42] improve multiple lines compatibility * [#42][42] improve multiple lines compatibility
* [#43][43] remove sub-package(runes) for gopkg compatiblity * [#43][43] remove sub-package(runes) for gopkg compatibility
* [#46][46] Auto complete with space prefixed line * [#46][46] Auto complete with space prefixed line
* [#48][48] support suspend process (ctrl+Z) * [#48][48] support suspend process (ctrl+Z)
* [#49][49] fix bug that check equals with previous command * [#49][49] fix bug that check equals with previous command
@ -19,12 +19,12 @@
### 1.2 - 2016-03-05 ### 1.2 - 2016-03-05
* Add a demo for checking password strength [example/readline-pass-strength](https://github.com/chzyer/readline/blob/master/example/readline-pass-strength/readline-pass-strength.go), , written by [@sahib](https://github.com/sahib) * Add a demo for checking password strength [example/readline-pass-strength](https://git.internal/re/readline/blob/master/example/readline-pass-strength/readline-pass-strength.go), , written by [@sahib](https://github.com/sahib)
* [#23][23], support stdin remapping * [#23][23], support stdin remapping
* [#27][27], add a `UniqueEditLine` to `Config`, which will erase the editing line after user submited it, usually use in IM. * [#27][27], add a `UniqueEditLine` to `Config`, which will erase the editing line after user submited it, usually use in IM.
* Add a demo for multiline [example/readline-multiline](https://github.com/chzyer/readline/blob/master/example/readline-multiline/readline-multiline.go) which can submit one SQL by multiple lines. * Add a demo for multiline [example/readline-multiline](https://git.internal/re/readline/blob/master/example/readline-multiline/readline-multiline.go) which can submit one SQL by multiple lines.
* Supports performs even stdin/stdout is not a tty. * Supports performs even stdin/stdout is not a tty.
* Add a new simple apis for single instance, check by [here](https://github.com/chzyer/readline/blob/master/std.go). It need to save history manually if using this api. * Add a new simple apis for single instance, check by [here](https://git.internal/re/readline/blob/master/std.go). It need to save history manually if using this api.
* [#28][28], fixes the history is not working as expected. * [#28][28], fixes the history is not working as expected.
* [#33][33], vim mode now support `c`, `d`, `x (delete character)`, `r (replace character)` * [#33][33], vim mode now support `c`, `d`, `x (delete character)`, `r (replace character)`
@ -42,17 +42,17 @@
* Initial public release. * Initial public release.
[12]: https://github.com/chzyer/readline/pull/12 [12]: https://git.internal/re/readline/pull/12
[17]: https://github.com/chzyer/readline/pull/17 [17]: https://git.internal/re/readline/pull/17
[23]: https://github.com/chzyer/readline/pull/23 [23]: https://git.internal/re/readline/pull/23
[27]: https://github.com/chzyer/readline/pull/27 [27]: https://git.internal/re/readline/pull/27
[28]: https://github.com/chzyer/readline/pull/28 [28]: https://git.internal/re/readline/pull/28
[33]: https://github.com/chzyer/readline/pull/33 [33]: https://git.internal/re/readline/pull/33
[38]: https://github.com/chzyer/readline/pull/38 [38]: https://git.internal/re/readline/pull/38
[42]: https://github.com/chzyer/readline/pull/42 [42]: https://git.internal/re/readline/pull/42
[43]: https://github.com/chzyer/readline/pull/43 [43]: https://git.internal/re/readline/pull/43
[46]: https://github.com/chzyer/readline/pull/46 [46]: https://git.internal/re/readline/pull/46
[48]: https://github.com/chzyer/readline/pull/48 [48]: https://git.internal/re/readline/pull/48
[49]: https://github.com/chzyer/readline/pull/49 [49]: https://git.internal/re/readline/pull/49
[53]: https://github.com/chzyer/readline/pull/53 [53]: https://git.internal/re/readline/pull/53
[60]: https://github.com/chzyer/readline/pull/60 [60]: https://git.internal/re/readline/pull/60

View File

@ -1,7 +1,7 @@
[![Build Status](https://travis-ci.org/chzyer/readline.svg?branch=master)](https://travis-ci.org/chzyer/readline) [![Build Status](https://travis-ci.org/chzyer/readline.svg?branch=master)](https://travis-ci.org/chzyer/readline)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md)
[![Version](https://img.shields.io/github/tag/chzyer/readline.svg)](https://github.com/chzyer/readline/releases) [![Version](https://img.shields.io/github/tag/chzyer/readline.svg)](https://git.internal/re/readline/releases)
[![GoDoc](https://godoc.org/github.com/chzyer/readline?status.svg)](https://godoc.org/github.com/chzyer/readline) [![GoDoc](https://godoc.org/git.internal/re/readline?status.svg)](https://godoc.org/git.internal/re/readline)
[![OpenCollective](https://opencollective.com/readline/badge/backers.svg)](#backers) [![OpenCollective](https://opencollective.com/readline/badge/backers.svg)](#backers)
[![OpenCollective](https://opencollective.com/readline/badge/sponsors.svg)](#sponsors) [![OpenCollective](https://opencollective.com/readline/badge/sponsors.svg)](#sponsors)
@ -11,7 +11,7 @@
<img src="https://raw.githubusercontent.com/chzyer/readline/assets/logo_f.png" /> <img src="https://raw.githubusercontent.com/chzyer/readline/assets/logo_f.png" />
</p> </p>
A powerful readline library in `Linux` `macOS` `Windows` A powerful readline library in `Linux` `macOS` `Windows` `Solaris` `AIX`
## Guide ## Guide
@ -21,14 +21,15 @@ A powerful readline library in `Linux` `macOS` `Windows`
## Repos using readline ## Repos using readline
[![cockroachdb](https://img.shields.io/github/stars/cockroachdb/cockroach.svg?label=cockroachdb/cockroach)](https://github.com/cockroachdb/cockroach) [![cockroachdb](https://img.shields.io/github/stars/cockroachdb/cockroach.svg?label=cockroachdb/cockroach)](https://github.com/cockroachdb/cockroach)
[![robertkrimen/otto](https://img.shields.io/github/stars/robertkrimen/otto.svg?label=robertkrimen/otto)](https://github.com/robertkrimen/otto)
[![empire](https://img.shields.io/github/stars/remind101/empire.svg?label=remind101/empire)](https://github.com/remind101/empire) [![empire](https://img.shields.io/github/stars/remind101/empire.svg?label=remind101/empire)](https://github.com/remind101/empire)
[![mehrdadrad/mylg](https://img.shields.io/github/stars/mehrdadrad/mylg.svg?label=mehrdadrad/mylg)](https://github.com/mehrdadrad/mylg)
[![knq/usql](https://img.shields.io/github/stars/knq/usql.svg?label=knq/usql)](https://github.com/knq/usql)
[![youtube/doorman](https://img.shields.io/github/stars/youtube/doorman.svg?label=youtube/doorman)](https://github.com/youtube/doorman) [![youtube/doorman](https://img.shields.io/github/stars/youtube/doorman.svg?label=youtube/doorman)](https://github.com/youtube/doorman)
[![bom-d-van/harp](https://img.shields.io/github/stars/bom-d-van/harp.svg?label=bom-d-van/harp)](https://github.com/bom-d-van/harp) [![bom-d-van/harp](https://img.shields.io/github/stars/bom-d-van/harp.svg?label=bom-d-van/harp)](https://github.com/bom-d-van/harp)
[![abiosoft/ishell](https://img.shields.io/github/stars/abiosoft/ishell.svg?label=abiosoft/ishell)](https://github.com/abiosoft/ishell) [![abiosoft/ishell](https://img.shields.io/github/stars/abiosoft/ishell.svg?label=abiosoft/ishell)](https://github.com/abiosoft/ishell)
[![robertkrimen/otto](https://img.shields.io/github/stars/robertkrimen/otto.svg?label=robertkrimen/otto)](https://github.com/robertkrimen/otto)
[![Netflix/hal-9001](https://img.shields.io/github/stars/Netflix/hal-9001.svg?label=Netflix/hal-9001)](https://github.com/Netflix/hal-9001) [![Netflix/hal-9001](https://img.shields.io/github/stars/Netflix/hal-9001.svg?label=Netflix/hal-9001)](https://github.com/Netflix/hal-9001)
[![docker/go-p9p](https://img.shields.io/github/stars/docker/go-p9p.svg?label=docker/go-p9p)](https://github.com/docker/go-p9p) [![docker/go-p9p](https://img.shields.io/github/stars/docker/go-p9p.svg?label=docker/go-p9p)](https://github.com/docker/go-p9p)
[![mehrdadrad/mylg](https://img.shields.io/github/stars/mehrdadrad/mylg.svg?label=mehrdadrad/mylg)](https://github.com/mehrdadrad/mylg)
## Feedback ## Feedback

View File

@ -25,6 +25,7 @@ const (
COLOR_BINTENSITY = 0x0080 COLOR_BINTENSITY = 0x0080
COMMON_LVB_UNDERSCORE = 0x8000 COMMON_LVB_UNDERSCORE = 0x8000
COMMON_LVB_BOLD = 0x0007
) )
var ColorTableFg = []word{ var ColorTableFg = []word{
@ -163,6 +164,8 @@ func (a *ANSIWriterCtx) ioloopEscSeq(w *bufio.Writer, r rune, argptr *[]string)
color |= ColorTableBg[c-40] color |= ColorTableBg[c-40]
} else if c == 4 { } else if c == 4 {
color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7] color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7]
} else if c == 1 {
color |= COMMON_LVB_BOLD | COLOR_FINTENSITY
} else { // unknown code treat as reset } else { // unknown code treat as reset
color = ColorTableFg[7] color = ColorTableFg[7]
} }

View File

@ -203,7 +203,9 @@ func (o *opCompleter) CompleteRefresh() {
// -1 to avoid reach the end of line // -1 to avoid reach the end of line
width := o.width - 1 width := o.width - 1
colNum := width / colWidth colNum := width / colWidth
if colNum != 0 {
colWidth += (width - (colWidth * colNum)) / colNum colWidth += (width - (colWidth * colNum)) / colNum
}
o.candidateColNum = colNum o.candidateColNum = colNum
buf := bufio.NewWriter(o.w) buf := bufio.NewWriter(o.w)
@ -219,7 +221,7 @@ func (o *opCompleter) CompleteRefresh() {
} }
buf.WriteString(string(same)) buf.WriteString(string(same))
buf.WriteString(string(c)) buf.WriteString(string(c))
buf.Write(bytes.Repeat([]byte(" "), colWidth-len(c)-len(same))) buf.Write(bytes.Repeat([]byte(" "), colWidth-runes.WidthAll(c)-runes.WidthAll(same)))
if inSelect { if inSelect {
buf.WriteString("\033[0m") buf.WriteString("\033[0m")

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/chzyer/readline" "git.internal/re/readline"
) )
func usage(w io.Writer) { func usage(w io.Writer) {
@ -61,6 +61,15 @@ var completer = readline.NewPrefixCompleter(
readline.PcItem("sleep"), readline.PcItem("sleep"),
) )
func filterInput(r rune) (rune, bool) {
switch r {
// block CtrlZ feature
case readline.CharCtrlZ:
return r, false
}
return r, true
}
func main() { func main() {
l, err := readline.NewEx(&readline.Config{ l, err := readline.NewEx(&readline.Config{
Prompt: "\033[31m»\033[0m ", Prompt: "\033[31m»\033[0m ",
@ -70,11 +79,13 @@ func main() {
EOFPrompt: "exit", EOFPrompt: "exit",
HistorySearchFold: true, HistorySearchFold: true,
FuncFilterInputRune: filterInput,
}) })
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer l.Close() defer l.Close()
l.CaptureExitSignal()
setPasswordCfg := l.GenPasswordConfig() setPasswordCfg := l.GenPasswordConfig()
setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
@ -127,12 +138,11 @@ func main() {
println("you set:", strconv.Quote(string(pswd))) println("you set:", strconv.Quote(string(pswd)))
} }
case strings.HasPrefix(line, "setprompt"): case strings.HasPrefix(line, "setprompt"):
prompt := line[10:] if len(line) <= 10 {
if prompt == "" {
log.Println("setprompt <prompt>") log.Println("setprompt <prompt>")
break break
} }
l.SetPrompt(prompt) l.SetPrompt(line[10:])
case strings.HasPrefix(line, "say"): case strings.HasPrefix(line, "say"):
line := strings.TrimSpace(line[3:]) line := strings.TrimSpace(line[3:])
if len(line) == 0 { if len(line) == 0 {

View File

@ -2,12 +2,12 @@ package main
import ( import (
"fmt" "fmt"
"log"
"math/rand" "math/rand"
"time" "time"
"github.com/chzyer/readline" "git.internal/re/readline"
) )
import "log"
func main() { func main() {
rl, err := readline.NewEx(&readline.Config{ rl, err := readline.NewEx(&readline.Config{

View File

@ -3,7 +3,7 @@ package main
import ( import (
"strings" "strings"
"github.com/chzyer/readline" "git.internal/re/readline"
) )
func main() { func main() {

View File

@ -22,8 +22,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/chzyer/readline" "git.internal/re/readline"
zxcvbn "github.com/nbutton23/zxcvbn-go"
) )
const ( const (
@ -50,30 +49,24 @@ func Colorize(msg string, color int) string {
func createStrengthPrompt(password []rune) string { func createStrengthPrompt(password []rune) string {
symbol, color := "", Red symbol, color := "", Red
strength := zxcvbn.PasswordStrength(string(password), nil)
switch { switch {
case strength.Score <= 1: case len(password) <= 1:
symbol = "✗" symbol = "✗"
color = Red color = Red
case strength.Score <= 2: case len(password) <= 3:
symbol = "⚡" symbol = "⚡"
color = Magenta color = Magenta
case strength.Score <= 3: case len(password) <= 5:
symbol = "⚠" symbol = "⚠"
color = Yellow color = Yellow
case strength.Score <= 4: default:
symbol = "✔" symbol = "✔"
color = Green color = Green
} }
prompt := Colorize(symbol, color) prompt := Colorize(symbol, color)
if strength.Entropy > 0 {
entropy := fmt.Sprintf(" %3.0f", strength.Entropy)
prompt += Colorize(entropy, Cyan)
} else {
prompt += Colorize(" ENT", Cyan) prompt += Colorize(" ENT", Cyan)
}
prompt += Colorize(" New Password: ", color) prompt += Colorize(" New Password: ", color)
return prompt return prompt

View File

@ -1,6 +1,6 @@
package main package main
import "github.com/chzyer/readline" import "git.internal/re/readline"
func main() { func main() {
if err := readline.DialRemote("tcp", ":12344"); err != nil { if err := readline.DialRemote("tcp", ":12344"); err != nil {

View File

@ -3,7 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/chzyer/readline" "git.internal/re/readline"
) )
func main() { func main() {

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module git.internal/re/readline
go 1.15
require (
github.com/chzyer/test v1.0.0
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -27,12 +27,14 @@ type opHistory struct {
current *list.Element current *list.Element
fd *os.File fd *os.File
fdLock sync.Mutex fdLock sync.Mutex
enable bool
} }
func newOpHistory(cfg *Config) (o *opHistory) { func newOpHistory(cfg *Config) (o *opHistory) {
o = &opHistory{ o = &opHistory{
cfg: cfg, cfg: cfg,
history: list.New(), history: list.New(),
enable: true,
} }
return o return o
} }
@ -117,7 +119,7 @@ func (o *opHistory) rewriteLocked() {
buf := bufio.NewWriter(fd) buf := bufio.NewWriter(fd)
for elem := o.history.Front(); elem != nil; elem = elem.Next() { for elem := o.history.Front(); elem != nil; elem = elem.Next() {
buf.WriteString(string(elem.Value.(*hisItem).Source)) buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n")
} }
buf.Flush() buf.Flush()
@ -223,6 +225,16 @@ func (o *opHistory) Next() ([]rune, bool) {
return runes.Copy(o.showItem(current.Value)), true return runes.Copy(o.showItem(current.Value)), true
} }
// Disable the current history
func (o *opHistory) Disable() {
o.enable = false
}
// Enable the current history
func (o *opHistory) Enable() {
o.enable = true
}
func (o *opHistory) debug() { func (o *opHistory) debug() {
Debug("-------") Debug("-------")
for item := o.history.Front(); item != nil; item = item.Next() { for item := o.history.Front(); item != nil; item = item.Next() {
@ -232,6 +244,12 @@ func (o *opHistory) debug() {
// save history // save history
func (o *opHistory) New(current []rune) (err error) { func (o *opHistory) New(current []rune) (err error) {
// history deactivated
if !o.enable {
return nil
}
current = runes.Copy(current) current = runes.Copy(current)
// if just use last command without modify // if just use last command without modify

View File

@ -3,6 +3,7 @@ package readline
import ( import (
"errors" "errors"
"io" "io"
"sync"
) )
var ( var (
@ -18,6 +19,7 @@ func (*InterruptError) Error() string {
} }
type Operation struct { type Operation struct {
m sync.Mutex
cfg *Config cfg *Config
t *Terminal t *Terminal
buf *RuneBuffer buf *RuneBuffer
@ -32,6 +34,10 @@ type Operation struct {
*opVim *opVim
} }
func (o *Operation) SetBuffer(what string) {
o.buf.Set([]rune(what))
}
type wrapWriter struct { type wrapWriter struct {
r *Operation r *Operation
t *Terminal t *Terminal
@ -66,7 +72,7 @@ func NewOperation(t *Terminal, cfg *Config) *Operation {
t: t, t: t,
buf: NewRuneBuffer(t, cfg.Prompt, cfg, width), buf: NewRuneBuffer(t, cfg.Prompt, cfg, width),
outchan: make(chan []rune), outchan: make(chan []rune),
errchan: make(chan error), errchan: make(chan error, 1),
} }
op.w = op.buf.w op.w = op.buf.w
op.SetConfig(cfg) op.SetConfig(cfg)
@ -91,11 +97,29 @@ func (o *Operation) SetMaskRune(r rune) {
o.buf.SetMask(r) o.buf.SetMask(r)
} }
func (o *Operation) GetConfig() *Config {
o.m.Lock()
cfg := *o.cfg
o.m.Unlock()
return &cfg
}
func (o *Operation) ioloop() { func (o *Operation) ioloop() {
for { for {
keepInSearchMode := false keepInSearchMode := false
keepInCompleteMode := false keepInCompleteMode := false
r := o.t.ReadRune() r := o.t.ReadRune()
if o.GetConfig().FuncFilterInputRune != nil {
var process bool
r, process = o.GetConfig().FuncFilterInputRune(r)
if !process {
o.t.KickRead()
o.buf.Refresh(nil) // to refresh the line
continue // ignore this rune
}
}
if r == 0 { // io.EOF if r == 0 { // io.EOF
if o.buf.Len() == 0 { if o.buf.Len() == 0 {
o.buf.Clean() o.buf.Clean()
@ -149,7 +173,7 @@ func (o *Operation) ioloop() {
o.buf.Refresh(nil) o.buf.Refresh(nil)
} }
case CharTab: case CharTab:
if o.cfg.AutoComplete == nil { if o.GetConfig().AutoComplete == nil {
o.t.Bell() o.t.Bell()
break break
} }
@ -213,13 +237,15 @@ func (o *Operation) ioloop() {
o.Refresh() o.Refresh()
case MetaBackspace, CharCtrlW: case MetaBackspace, CharCtrlW:
o.buf.BackEscapeWord() o.buf.BackEscapeWord()
case CharCtrlY:
o.buf.Yank()
case CharEnter, CharCtrlJ: case CharEnter, CharCtrlJ:
if o.IsSearchMode() { if o.IsSearchMode() {
o.ExitSearchMode(false) o.ExitSearchMode(false)
} }
o.buf.MoveToLineEnd() o.buf.MoveToLineEnd()
var data []rune var data []rune
if !o.cfg.UniqueEditLine { if !o.GetConfig().UniqueEditLine {
o.buf.WriteRune('\n') o.buf.WriteRune('\n')
data = o.buf.Reset() data = o.buf.Reset()
data = data[:len(data)-1] // trim \n data = data[:len(data)-1] // trim \n
@ -228,7 +254,7 @@ func (o *Operation) ioloop() {
data = o.buf.Reset() data = o.buf.Reset()
} }
o.outchan <- data o.outchan <- data
if !o.cfg.DisableAutoSaveHistory { if !o.GetConfig().DisableAutoSaveHistory {
// ignore IO error // ignore IO error
_ = o.history.New(data) _ = o.history.New(data)
} else { } else {
@ -262,14 +288,14 @@ func (o *Operation) ioloop() {
} }
// treat as EOF // treat as EOF
if !o.cfg.UniqueEditLine { if !o.GetConfig().UniqueEditLine {
o.buf.WriteString(o.cfg.EOFPrompt + "\n") o.buf.WriteString(o.GetConfig().EOFPrompt + "\n")
} }
o.buf.Reset() o.buf.Reset()
isUpdateHistory = false isUpdateHistory = false
o.history.Revert() o.history.Revert()
o.errchan <- io.EOF o.errchan <- io.EOF
if o.cfg.UniqueEditLine { if o.GetConfig().UniqueEditLine {
o.buf.Clean() o.buf.Clean()
} }
case CharInterrupt: case CharInterrupt:
@ -286,12 +312,12 @@ func (o *Operation) ioloop() {
} }
o.buf.MoveToLineEnd() o.buf.MoveToLineEnd()
o.buf.Refresh(nil) o.buf.Refresh(nil)
hint := o.cfg.InterruptPrompt + "\n" hint := o.GetConfig().InterruptPrompt + "\n"
if !o.cfg.UniqueEditLine { if !o.GetConfig().UniqueEditLine {
o.buf.WriteString(hint) o.buf.WriteString(hint)
} }
remain := o.buf.Reset() remain := o.buf.Reset()
if !o.cfg.UniqueEditLine { if !o.GetConfig().UniqueEditLine {
remain = remain[:len(remain)-len([]rune(hint))] remain = remain[:len(remain)-len([]rune(hint))]
} }
isUpdateHistory = false isUpdateHistory = false
@ -310,13 +336,15 @@ func (o *Operation) ioloop() {
} }
} }
if o.cfg.Listener != nil { listener := o.GetConfig().Listener
newLine, newPos, ok := o.cfg.Listener.OnChange(o.buf.Runes(), o.buf.Pos(), r) if listener != nil {
newLine, newPos, ok := listener.OnChange(o.buf.Runes(), o.buf.Pos(), r)
if ok { if ok {
o.buf.SetWithIdx(newPos, newLine) o.buf.SetWithIdx(newPos, newLine)
} }
} }
o.m.Lock()
if !keepInSearchMode && o.IsSearchMode() { if !keepInSearchMode && o.IsSearchMode() {
o.ExitSearchMode(false) o.ExitSearchMode(false)
o.buf.Refresh(nil) o.buf.Refresh(nil)
@ -333,15 +361,16 @@ func (o *Operation) ioloop() {
// it will cause null history // it will cause null history
o.history.Update(o.buf.Runes(), false) o.history.Update(o.buf.Runes(), false)
} }
o.m.Unlock()
} }
} }
func (o *Operation) Stderr() io.Writer { func (o *Operation) Stderr() io.Writer {
return &wrapWriter{target: o.cfg.Stderr, r: o, t: o.t} return &wrapWriter{target: o.GetConfig().Stderr, r: o, t: o.t}
} }
func (o *Operation) Stdout() io.Writer { func (o *Operation) Stdout() io.Writer {
return &wrapWriter{target: o.cfg.Stdout, r: o, t: o.t} return &wrapWriter{target: o.GetConfig().Stdout, r: o, t: o.t}
} }
func (o *Operation) String() (string, error) { func (o *Operation) String() (string, error) {
@ -353,8 +382,9 @@ func (o *Operation) Runes() ([]rune, error) {
o.t.EnterRawMode() o.t.EnterRawMode()
defer o.t.ExitRawMode() defer o.t.ExitRawMode()
if o.cfg.Listener != nil { listener := o.GetConfig().Listener
o.cfg.Listener.OnChange(nil, 0, 0) if listener != nil {
listener.OnChange(nil, 0, 0)
} }
o.buf.Refresh(nil) // print prompt o.buf.Refresh(nil) // print prompt
@ -406,6 +436,10 @@ func (o *Operation) Slice() ([]byte, error) {
} }
func (o *Operation) Close() { func (o *Operation) Close() {
select {
case o.errchan <- io.EOF:
default:
}
o.history.Close() o.history.Close()
} }
@ -422,6 +456,8 @@ func (o *Operation) IsNormalMode() bool {
} }
func (op *Operation) SetConfig(cfg *Config) (*Config, error) { func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
op.m.Lock()
defer op.m.Unlock()
if op.cfg == cfg { if op.cfg == cfg {
return op.cfg, nil return op.cfg, nil
} }
@ -489,3 +525,13 @@ func (d *DumpListener) OnChange(line []rune, pos int, key rune) (newLine []rune,
type Listener interface { type Listener interface {
OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
} }
type Painter interface {
Paint(line []rune, pos int) []rune
}
type defaultPainter struct{}
func (p *defaultPainter) Paint(line []rune, _ int) []rune {
return line
}

View File

@ -25,6 +25,7 @@ func (o *opPassword) PasswordConfig() *Config {
InterruptPrompt: "\n", InterruptPrompt: "\n",
EOFPrompt: "\n", EOFPrompt: "\n",
HistoryLimit: -1, HistoryLimit: -1,
Painter: &defaultPainter{},
Stdout: o.o.cfg.Stdout, Stdout: o.o.cfg.Stdout,
Stderr: o.o.cfg.Stderr, Stderr: o.o.cfg.Stderr,

View File

@ -17,7 +17,9 @@
// //
package readline package readline
import "io" import (
"io"
)
type Instance struct { type Instance struct {
Config *Config Config *Config
@ -44,6 +46,8 @@ type Config struct {
// NOTE: Listener will be triggered by (nil, 0, 0) immediately // NOTE: Listener will be triggered by (nil, 0, 0) immediately
Listener Listener Listener Listener
Painter Painter
// If VimMode is true, readline will in vim.insert mode by default // If VimMode is true, readline will in vim.insert mode by default
VimMode bool VimMode bool
@ -52,7 +56,8 @@ type Config struct {
FuncGetWidth func() int FuncGetWidth func() int
Stdin io.Reader Stdin io.ReadCloser
StdinWriter io.Writer
Stdout io.Writer Stdout io.Writer
Stderr io.Writer Stderr io.Writer
@ -63,6 +68,10 @@ type Config struct {
// it use in IM usually. // it use in IM usually.
UniqueEditLine bool UniqueEditLine bool
// filter input runes (may be used to disable CtrlZ or for translating some keys to different actions)
// -> output = new (translated) rune and true/false if continue with processing this one
FuncFilterInputRune func(rune) (rune, bool)
// force use interactive even stdout is not a tty // force use interactive even stdout is not a tty
FuncIsTerminal func() bool FuncIsTerminal func() bool
FuncMakeRaw func() error FuncMakeRaw func() error
@ -91,6 +100,9 @@ func (c *Config) Init() error {
if c.Stdin == nil { if c.Stdin == nil {
c.Stdin = NewCancelableStdin(Stdin) c.Stdin = NewCancelableStdin(Stdin)
} }
c.Stdin, c.StdinWriter = NewFillableStdin(c.Stdin)
if c.Stdout == nil { if c.Stdout == nil {
c.Stdout = Stdout c.Stdout = Stdout
} }
@ -145,12 +157,19 @@ func (c *Config) SetListener(f func(line []rune, pos int, key rune) (newLine []r
c.Listener = FuncListener(f) c.Listener = FuncListener(f)
} }
func (c *Config) SetPainter(p Painter) {
c.Painter = p
}
func NewEx(cfg *Config) (*Instance, error) { func NewEx(cfg *Config) (*Instance, error) {
t, err := NewTerminal(cfg) t, err := NewTerminal(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
rl := t.Readline() rl := t.Readline()
if cfg.Painter == nil {
cfg.Painter = &defaultPainter{}
}
return &Instance{ return &Instance{
Config: cfg, Config: cfg,
Terminal: t, Terminal: t,
@ -238,6 +257,11 @@ func (i *Instance) Readline() (string, error) {
return i.Operation.String() return i.Operation.String()
} }
func (i *Instance) ReadlineWithDefault(what string) (string, error) {
i.Operation.SetBuffer(what)
return i.Operation.String()
}
func (i *Instance) SaveHistory(content string) error { func (i *Instance) SaveHistory(content string) error {
return i.Operation.SaveHistory(content) return i.Operation.SaveHistory(content)
} }
@ -248,13 +272,24 @@ func (i *Instance) ReadSlice() ([]byte, error) {
} }
// we must make sure that call Close() before process exit. // we must make sure that call Close() before process exit.
// if there has a pending reading operation, that reading will be interrupted.
// so you can capture the signal and call Instance.Close(), it's thread-safe.
func (i *Instance) Close() error { func (i *Instance) Close() error {
i.Config.Stdin.Close()
i.Operation.Close()
if err := i.Terminal.Close(); err != nil { if err := i.Terminal.Close(); err != nil {
return err return err
} }
i.Operation.Close()
return nil return nil
} }
// call CaptureExitSignal when you want readline exit gracefully.
func (i *Instance) CaptureExitSignal() {
CaptureExitSignal(func() {
i.Close()
})
}
func (i *Instance) Clean() { func (i *Instance) Clean() {
i.Operation.Clean() i.Operation.Clean()
} }
@ -263,6 +298,20 @@ func (i *Instance) Write(b []byte) (int, error) {
return i.Stdout().Write(b) return i.Stdout().Write(b)
} }
// WriteStdin prefill the next Stdin fetch
// Next time you call ReadLine() this value will be writen before the user input
// ie :
// i := readline.New()
// i.WriteStdin([]byte("test"))
// _, _= i.Readline()
//
// gives
//
// > test[cursor]
func (i *Instance) WriteStdin(val []byte) (int, error) {
return i.Terminal.WriteStdin(val)
}
func (i *Instance) SetConfig(cfg *Config) *Config { func (i *Instance) SetConfig(cfg *Config) *Config {
if i.Config == cfg { if i.Config == cfg {
return cfg return cfg
@ -277,3 +326,13 @@ func (i *Instance) SetConfig(cfg *Config) *Config {
func (i *Instance) Refresh() { func (i *Instance) Refresh() {
i.Operation.Refresh() i.Operation.Refresh()
} }
// HistoryDisable the save of the commands into the history
func (i *Instance) HistoryDisable() {
i.Operation.history.Disable()
}
// HistoryEnable the save of the commands into the history (default on)
func (i *Instance) HistoryEnable() {
i.Operation.history.Enable()
}

View File

@ -189,11 +189,12 @@ loop:
} }
} }
func (r *RemoteSvr) Close() { func (r *RemoteSvr) Close() error {
if atomic.CompareAndSwapInt32(&r.closed, 0, 1) { if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
close(r.stopChan) close(r.stopChan)
r.conn.Close() r.conn.Close()
} }
return nil
} }
func (r *RemoteSvr) readLoop(buf *bufio.Reader) { func (r *RemoteSvr) readLoop(buf *bufio.Reader) {

View File

@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"io" "io"
"strconv"
"strings" "strings"
"sync" "sync"
) )
@ -29,9 +30,15 @@ type RuneBuffer struct {
offset string offset string
lastKill []rune
sync.Mutex sync.Mutex
} }
func (r *RuneBuffer) pushKill(text []rune) {
r.lastKill = append([]rune{}, text...)
}
func (r *RuneBuffer) OnWidthChange(newWidth int) { func (r *RuneBuffer) OnWidthChange(newWidth int) {
r.Lock() r.Lock()
r.width = newWidth r.width = newWidth
@ -187,6 +194,7 @@ func (r *RuneBuffer) Replace(ch rune) {
func (r *RuneBuffer) Erase() { func (r *RuneBuffer) Erase() {
r.Refresh(func() { r.Refresh(func() {
r.idx = 0 r.idx = 0
r.pushKill(r.buf[:])
r.buf = r.buf[:0] r.buf = r.buf[:0]
}) })
} }
@ -196,6 +204,7 @@ func (r *RuneBuffer) Delete() (success bool) {
if r.idx == len(r.buf) { if r.idx == len(r.buf) {
return return
} }
r.pushKill(r.buf[r.idx : r.idx+1])
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...) r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
success = true success = true
}) })
@ -212,6 +221,7 @@ func (r *RuneBuffer) DeleteWord() {
} }
for i := init + 1; i < len(r.buf); i++ { for i := init + 1; i < len(r.buf); i++ {
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
r.pushKill(r.buf[r.idx : i-1])
r.Refresh(func() { r.Refresh(func() {
r.buf = append(r.buf[:r.idx], r.buf[i-1:]...) r.buf = append(r.buf[:r.idx], r.buf[i-1:]...)
}) })
@ -247,6 +257,7 @@ func (r *RuneBuffer) KillFront() {
} }
length := len(r.buf) - r.idx length := len(r.buf) - r.idx
r.pushKill(r.buf[:r.idx])
copy(r.buf[:length], r.buf[r.idx:]) copy(r.buf[:length], r.buf[r.idx:])
r.idx = 0 r.idx = 0
r.buf = r.buf[:length] r.buf = r.buf[:length]
@ -255,6 +266,7 @@ func (r *RuneBuffer) KillFront() {
func (r *RuneBuffer) Kill() { func (r *RuneBuffer) Kill() {
r.Refresh(func() { r.Refresh(func() {
r.pushKill(r.buf[r.idx:])
r.buf = r.buf[:r.idx] r.buf = r.buf[:r.idx]
}) })
} }
@ -321,6 +333,7 @@ func (r *RuneBuffer) BackEscapeWord() {
} }
for i := r.idx - 1; i > 0; i-- { for i := r.idx - 1; i > 0; i-- {
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) { if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
r.pushKill(r.buf[i:r.idx])
r.buf = append(r.buf[:i], r.buf[r.idx:]...) r.buf = append(r.buf[:i], r.buf[r.idx:]...)
r.idx = i r.idx = i
return return
@ -332,6 +345,20 @@ func (r *RuneBuffer) BackEscapeWord() {
}) })
} }
func (r *RuneBuffer) Yank() {
if len(r.lastKill) == 0 {
return
}
r.Refresh(func() {
buf := make([]rune, 0, len(r.buf)+len(r.lastKill))
buf = append(buf, r.buf[:r.idx]...)
buf = append(buf, r.lastKill...)
buf = append(buf, r.buf[r.idx:]...)
r.buf = buf
r.idx += len(r.lastKill)
})
}
func (r *RuneBuffer) Backspace() { func (r *RuneBuffer) Backspace() {
r.Refresh(func() { r.Refresh(func() {
if r.idx == 0 { if r.idx == 0 {
@ -460,28 +487,58 @@ func (r *RuneBuffer) output() []byte {
buf.Write([]byte(string(r.cfg.MaskRune))) buf.Write([]byte(string(r.cfg.MaskRune)))
} }
if len(r.buf) > r.idx { if len(r.buf) > r.idx {
buf.Write(runes.Backspace(r.buf[r.idx:])) buf.Write(r.getBackspaceSequence())
} }
} else { } else {
for idx := range r.buf { for _, e := range r.cfg.Painter.Paint(r.buf, r.idx) {
if r.buf[idx] == '\t' { if e == '\t' {
buf.WriteString(strings.Repeat(" ", TabWidth)) buf.WriteString(strings.Repeat(" ", TabWidth))
} else { } else {
buf.WriteRune(r.buf[idx]) buf.WriteRune(e)
} }
} }
if r.isInLineEdge() { if r.isInLineEdge() {
buf.Write([]byte(" \b")) buf.Write([]byte(" \b"))
} }
} }
// cursor position
if len(r.buf) > r.idx { if len(r.buf) > r.idx {
buf.Write(runes.Backspace(r.buf[r.idx:])) buf.Write(r.getBackspaceSequence())
} }
return buf.Bytes() return buf.Bytes()
} }
func (r *RuneBuffer) getBackspaceSequence() []byte {
var sep = map[int]bool{}
var i int
for {
if i >= runes.WidthAll(r.buf) {
break
}
if i == 0 {
i -= r.promptLen()
}
i += r.width
sep[i] = true
}
var buf []byte
for i := len(r.buf); i > r.idx; i-- {
// move input to the left of one
buf = append(buf, '\b')
if sep[i] {
// up one line, go to the start of the line and move cursor right to the end (r.width)
buf = append(buf, "\033[A\r"+"\033["+strconv.Itoa(r.width)+"C"...)
}
}
return buf
}
func (r *RuneBuffer) Reset() []rune { func (r *RuneBuffer) Reset() []rune {
ret := runes.Copy(r.buf) ret := runes.Copy(r.buf)
r.buf = r.buf[:0] r.buf = r.buf[:0]

View File

@ -1,6 +1,6 @@
// deprecated. // deprecated.
// see https://github.com/chzyer/readline/issues/43 // see https://git.internal/re/readline/issues/43
// use github.com/chzyer/readline/runes.go // use git.internal/re/readline/runes.go
package runes package runes
import ( import (

83
std.go
View File

@ -4,6 +4,7 @@ import (
"io" "io"
"os" "os"
"sync" "sync"
"sync/atomic"
) )
var ( var (
@ -69,6 +70,7 @@ type CancelableStdin struct {
r io.Reader r io.Reader
mutex sync.Mutex mutex sync.Mutex
stop chan struct{} stop chan struct{}
closed int32
notify chan struct{} notify chan struct{}
data []byte data []byte
read int read int
@ -91,7 +93,11 @@ loop:
select { select {
case <-c.notify: case <-c.notify:
c.read, c.err = c.r.Read(c.data) c.read, c.err = c.r.Read(c.data)
c.notify <- struct{}{} select {
case c.notify <- struct{}{}:
case <-c.stop:
break loop
}
case <-c.stop: case <-c.stop:
break loop break loop
} }
@ -101,9 +107,16 @@ loop:
func (c *CancelableStdin) Read(b []byte) (n int, err error) { func (c *CancelableStdin) Read(b []byte) (n int, err error) {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
if atomic.LoadInt32(&c.closed) == 1 {
return 0, io.EOF
}
c.data = b c.data = b
c.notify <- struct{}{} select {
case c.notify <- struct{}{}:
case <-c.stop:
return 0, io.EOF
}
select { select {
case <-c.notify: case <-c.notify:
return c.read, c.err return c.read, c.err
@ -113,6 +126,72 @@ func (c *CancelableStdin) Read(b []byte) (n int, err error) {
} }
func (c *CancelableStdin) Close() error { func (c *CancelableStdin) Close() error {
if atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
close(c.stop) close(c.stop)
}
return nil
}
// FillableStdin is a stdin reader which can prepend some data before
// reading into the real stdin
type FillableStdin struct {
sync.Mutex
stdin io.Reader
stdinBuffer io.ReadCloser
buf []byte
bufErr error
}
// NewFillableStdin gives you FillableStdin
func NewFillableStdin(stdin io.Reader) (io.ReadCloser, io.Writer) {
r, w := io.Pipe()
s := &FillableStdin{
stdinBuffer: r,
stdin: stdin,
}
s.ioloop()
return s, w
}
func (s *FillableStdin) ioloop() {
go func() {
for {
bufR := make([]byte, 100)
var n int
n, s.bufErr = s.stdinBuffer.Read(bufR)
if s.bufErr != nil {
if s.bufErr == io.ErrClosedPipe {
break
}
}
s.Lock()
s.buf = append(s.buf, bufR[:n]...)
s.Unlock()
}
}()
}
// Read will read from the local buffer and if no data, read from stdin
func (s *FillableStdin) Read(p []byte) (n int, err error) {
s.Lock()
i := len(s.buf)
if len(p) < i {
i = len(p)
}
if i > 0 {
n := copy(p, s.buf)
s.buf = s.buf[:0]
cerr := s.bufErr
s.bufErr = nil
s.Unlock()
return n, cerr
}
s.Unlock()
n, err = s.stdin.Read(p)
return n, err
}
func (s *FillableStdin) Close() error {
s.stdinBuffer.Close()
return nil return nil
} }

49
term.go
View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd // +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris
// Package terminal provides support functions for dealing with terminals, as // Package terminal provides support functions for dealing with terminals, as
// commonly found on UNIX systems. // commonly found on UNIX systems.
@ -19,19 +19,17 @@ package readline
import ( import (
"io" "io"
"syscall" "syscall"
"unsafe"
) )
// State contains the state of a terminal. // State contains the state of a terminal.
type State struct { type State struct {
termios syscall.Termios termios Termios
} }
// IsTerminal returns true if the given file descriptor is a terminal. // IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal(fd int) bool { func IsTerminal(fd int) bool {
var termios syscall.Termios _, err := getTermios(fd)
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) return err == nil
return err == 0
} }
// MakeRaw put the terminal connected to the given file descriptor into raw // MakeRaw put the terminal connected to the given file descriptor into raw
@ -39,8 +37,11 @@ func IsTerminal(fd int) bool {
// restored. // restored.
func MakeRaw(fd int) (*State, error) { func MakeRaw(fd int) (*State, error) {
var oldState State var oldState State
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 {
if termios, err := getTermios(fd); err != nil {
return nil, err return nil, err
} else {
oldState.termios = *termios
} }
newState := oldState.termios newState := oldState.termios
@ -52,47 +53,35 @@ func MakeRaw(fd int) (*State, error) {
newState.Cflag &^= syscall.CSIZE | syscall.PARENB newState.Cflag &^= syscall.CSIZE | syscall.PARENB
newState.Cflag |= syscall.CS8 newState.Cflag |= syscall.CS8
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { newState.Cc[syscall.VMIN] = 1
return nil, err newState.Cc[syscall.VTIME] = 0
}
return &oldState, nil return &oldState, setTermios(fd, &newState)
} }
// GetState returns the current state of a terminal which may be useful to // GetState returns the current state of a terminal which may be useful to
// restore the terminal after a signal. // restore the terminal after a signal.
func GetState(fd int) (*State, error) { func GetState(fd int) (*State, error) {
var oldState State termios, err := getTermios(fd)
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState.termios)), 0, 0, 0); err != 0 { if err != nil {
return nil, err return nil, err
} }
return &oldState, nil return &State{termios: *termios}, nil
} }
// Restore restores the terminal connected to the given file descriptor to a // Restore restores the terminal connected to the given file descriptor to a
// previous state. // previous state.
func restoreTerm(fd int, state *State) error { func restoreTerm(fd int, state *State) error {
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&state.termios)), 0, 0, 0) return setTermios(fd, &state.termios)
return err
}
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (width, height int, err error) {
var dimensions [4]uint16
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 {
return -1, -1, err
}
return int(dimensions[1]), int(dimensions[0]), nil
} }
// ReadPassword reads a line of input from a terminal without local echo. This // ReadPassword reads a line of input from a terminal without local echo. This
// is commonly used for inputting passwords and other sensitive data. The slice // is commonly used for inputting passwords and other sensitive data. The slice
// returned does not include the \n. // returned does not include the \n.
func ReadPassword(fd int) ([]byte, error) { func ReadPassword(fd int) ([]byte, error) {
var oldState syscall.Termios oldState, err := getTermios(fd)
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0); err != 0 { if err != nil {
return nil, err return nil, err
} }
@ -100,12 +89,12 @@ func ReadPassword(fd int) ([]byte, error) {
newState.Lflag &^= syscall.ECHO newState.Lflag &^= syscall.ECHO
newState.Lflag |= syscall.ICANON | syscall.ISIG newState.Lflag |= syscall.ICANON | syscall.ISIG
newState.Iflag |= syscall.ICRNL newState.Iflag |= syscall.ICRNL
if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { if err := setTermios(fd, newState); err != nil {
return nil, err return nil, err
} }
defer func() { defer func() {
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(&oldState)), 0, 0, 0) setTermios(fd, oldState)
}() }()
var buf [16]byte var buf [16]byte

View File

@ -6,7 +6,24 @@
package readline package readline
import "syscall" import (
"syscall"
"unsafe"
)
const ioctlReadTermios = syscall.TIOCGETA func getTermios(fd int) (*Termios, error) {
const ioctlWriteTermios = syscall.TIOCSETA termios := new(Termios)
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCGETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
if err != 0 {
return nil, err
}
return termios, nil
}
func setTermios(fd int, termios *Termios) error {
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCSETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
if err != 0 {
return err
}
return nil
}

View File

@ -4,8 +4,30 @@
package readline package readline
import (
"syscall"
"unsafe"
)
// These constants are declared here, rather than importing // These constants are declared here, rather than importing
// them from the syscall package as some syscall packages, even // them from the syscall package as some syscall packages, even
// on linux, for example gccgo, do not declare them. // on linux, for example gccgo, do not declare them.
const ioctlReadTermios = 0x5401 // syscall.TCGETS const ioctlReadTermios = 0x5401 // syscall.TCGETS
const ioctlWriteTermios = 0x5402 // syscall.TCSETS const ioctlWriteTermios = 0x5402 // syscall.TCSETS
func getTermios(fd int) (*Termios, error) {
termios := new(Termios)
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
if err != 0 {
return nil, err
}
return termios, nil
}
func setTermios(fd int, termios *Termios) error {
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
if err != 0 {
return err
}
return nil
}

32
term_nosyscall6.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2013 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.
// +build aix os400 solaris
package readline
import "golang.org/x/sys/unix"
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (int, int, error) {
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
if err != nil {
return 0, 0, err
}
return int(ws.Col), int(ws.Row), nil
}
type Termios unix.Termios
func getTermios(fd int) (*Termios, error) {
termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
if err != nil {
return nil, err
}
return (*Termios)(termios), nil
}
func setTermios(fd int, termios *Termios) error {
return unix.IoctlSetTermios(fd, unix.TCSETSF, (*unix.Termios)(termios))
}

24
term_unix.go Normal file
View File

@ -0,0 +1,24 @@
// 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.
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd
package readline
import (
"syscall"
"unsafe"
)
type Termios syscall.Termios
// GetSize returns the dimensions of the given terminal.
func GetSize(fd int) (int, int, error) {
var dimensions [4]uint16
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0)
if err != 0 {
return 0, 0, err
}
return int(dimensions[1]), int(dimensions[0]), nil
}

View File

@ -10,6 +10,7 @@ import (
) )
type Terminal struct { type Terminal struct {
m sync.Mutex
cfg *Config cfg *Config
outchan chan rune outchan chan rune
closed int32 closed int32
@ -64,6 +65,12 @@ func (t *Terminal) Write(b []byte) (int, error) {
return t.cfg.Stdout.Write(b) return t.cfg.Stdout.Write(b)
} }
// WriteStdin prefill the next Stdin fetch
// Next time you call ReadLine() this value will be writen before the user input
func (t *Terminal) WriteStdin(b []byte) (int, error) {
return t.cfg.StdinWriter.Write(b)
}
type termSize struct { type termSize struct {
left int left int
top int top int
@ -118,10 +125,11 @@ func (t *Terminal) ioloop() {
var ( var (
isEscape bool isEscape bool
isEscapeEx bool isEscapeEx bool
isEscapeSS3 bool
expectNextChar bool expectNextChar bool
) )
buf := bufio.NewReader(t.cfg.Stdin) buf := bufio.NewReader(t.getStdin())
for { for {
if !expectNextChar { if !expectNextChar {
atomic.StoreInt32(&t.isReading, 0) atomic.StoreInt32(&t.isReading, 0)
@ -145,9 +153,15 @@ func (t *Terminal) ioloop() {
if isEscape { if isEscape {
isEscape = false isEscape = false
if r == CharEscapeEx { if r == CharEscapeEx {
// ^][
expectNextChar = true expectNextChar = true
isEscapeEx = true isEscapeEx = true
continue continue
} else if r == CharO {
// ^]O
expectNextChar = true
isEscapeSS3 = true
continue
} }
r = escapeKey(r, buf) r = escapeKey(r, buf)
} else if isEscapeEx { } else if isEscapeEx {
@ -170,6 +184,15 @@ func (t *Terminal) ioloop() {
expectNextChar = true expectNextChar = true
continue continue
} }
} else if isEscapeSS3 {
isEscapeSS3 = false
if key := readEscKey(r, buf); key != nil {
r = escapeSS3Key(key)
}
if r == 0 {
expectNextChar = true
continue
}
} }
expectNextChar = true expectNextChar = true
@ -206,10 +229,26 @@ func (t *Terminal) Close() error {
return t.ExitRawMode() return t.ExitRawMode()
} }
func (t *Terminal) GetConfig() *Config {
t.m.Lock()
cfg := *t.cfg
t.m.Unlock()
return &cfg
}
func (t *Terminal) getStdin() io.Reader {
t.m.Lock()
r := t.cfg.Stdin
t.m.Unlock()
return r
}
func (t *Terminal) SetConfig(c *Config) error { func (t *Terminal) SetConfig(c *Config) error {
if err := c.Init(); err != nil { if err := c.Init(); err != nil {
return err return err
} }
t.m.Lock()
t.cfg = c t.cfg = c
t.m.Unlock()
return nil return nil
} }

View File

@ -6,9 +6,11 @@ import (
"container/list" "container/list"
"fmt" "fmt"
"os" "os"
"os/signal"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"unicode" "unicode"
) )
@ -38,8 +40,10 @@ const (
CharTranspose = 20 CharTranspose = 20
CharCtrlU = 21 CharCtrlU = 21
CharCtrlW = 23 CharCtrlW = 23
CharCtrlY = 25
CharCtrlZ = 26 CharCtrlZ = 26
CharEsc = 27 CharEsc = 27
CharO = 79
CharEscapeEx = 91 CharEscapeEx = 91
CharBackspace = 127 CharBackspace = 127
) )
@ -82,7 +86,9 @@ func Restore(fd int, state *State) error {
if err != nil { if err != nil {
// errno 0 means everything is ok :) // errno 0 means everything is ok :)
if err.Error() == "errno 0" { if err.Error() == "errno 0" {
err = nil return nil
} else {
return err
} }
} }
return nil return nil
@ -118,6 +124,27 @@ func escapeExKey(key *escapeKeyPair) rune {
return r return r
} }
// translate EscOX SS3 codes for up/down/etc.
func escapeSS3Key(key *escapeKeyPair) rune {
var r rune
switch key.typ {
case 'D':
r = CharBackward
case 'C':
r = CharForward
case 'A':
r = CharPrev
case 'B':
r = CharNext
case 'H':
r = CharLineStart
case 'F':
r = CharLineEnd
default:
}
return r
}
type escapeKeyPair struct { type escapeKeyPair struct {
attr string attr string
typ rune typ rune
@ -272,3 +299,13 @@ func Debug(o ...interface{}) {
fmt.Fprintln(f, o...) fmt.Fprintln(f, o...)
f.Close() f.Close()
} }
func CaptureExitSignal(f func()) {
cSignal := make(chan os.Signal, 1)
signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
go func() {
for range cSignal {
f()
}
}()
}

View File

@ -1,4 +1,4 @@
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd // +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris
package readline package readline
@ -8,7 +8,6 @@ import (
"os/signal" "os/signal"
"sync" "sync"
"syscall" "syscall"
"unsafe"
) )
type winsize struct { type winsize struct {
@ -30,17 +29,11 @@ func SuspendMe() {
// get width of the terminal // get width of the terminal
func getWidth(stdoutFd int) int { func getWidth(stdoutFd int) int {
ws := &winsize{} cols, _, err := GetSize(stdoutFd)
retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL, if err != nil {
uintptr(stdoutFd),
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(ws)))
if int(retCode) == -1 {
_ = errno
return -1 return -1
} }
return int(ws.Col) return cols
} }
func GetScreenWidth() int { func GetScreenWidth() int {

2
vim.go
View File

@ -72,6 +72,8 @@ func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, h
case 'l': case 'l':
rb.Delete() rb.Delete()
} }
case 'p':
rb.Yank()
case 'b', 'B': case 'b', 'B':
rb.MoveToPrevWord() rb.MoveToPrevWord()
case 'w', 'W': case 'w', 'W':