forked from mirror/readline
Compare commits
250 Commits
Author | SHA1 | Date |
---|---|---|
re | 295ca315c5 | |
ChenYe | 7f93d88cd5 | |
Thomas O'Dowd | 8e4bd417b9 | |
ChenYe | 80e2d1961b | |
Derick Rethans | c34436b973 | |
Clément Chigot | a5e9f81cc2 | |
ChenYe | a11d8f26cf | |
Jim-wiselike | 2972be24d4 | |
soopsio | f6d7a1f6fb | |
François-Xavier Aguessy | 40d6036c33 | |
Abiola Ibrahim | a4d5111b61 | |
Eugene Apollonsky | 6a4bc7b4fe | |
mrsinham | 9cc74fe5ad | |
mrsinham | af545c8af6 | |
mrsinham | aee0fa669f | |
Saeed Rasooli | aa9ed7db49 | |
Jelmer Snoeck | c6c3e8d906 | |
Thomas Bradford | 707fd8ecaa | |
Sagar Mittal | 9f56defe66 | |
Edward Betts | 045fff973b | |
Davor Kapsa | 8d510b4136 | |
Jordan Lewis | 01c4e90c35 | |
Paul Tagliamonte | 41eea22f71 | |
Gereon Frey | 8a1389155f | |
Kenneth Shaw | 784bd70fe0 | |
Jiří Setnička | f892600c35 | |
chzyer | eef24db1d5 | |
Remi Reuvekamp | c914be64f0 | |
chzyer | 25c2772d5f | |
chzyer | 94eaec69a7 | |
Ben Browning | bc5e387904 | |
chzyer | 283f5429f7 | |
chzyer | 820d6f2766 | |
chzyer | 31ab3eee0c | |
chzyer | 6cbf970b0b | |
chzyer | edfa7c9dbf | |
chzyer | bc5c91eb5b | |
chzyer | f533ef1caa | |
chzyer | 31eb6be473 | |
chzyer | 4ffff9f41c | |
chzyer | 6cbc078c57 | |
chzyer | 27fdf0b285 | |
Mehrdad Arshad Rad | 8159bd380c | |
Nathan VanBenschoten | a193146c91 | |
chzyer | c144c8dde4 | |
John Cramb | a0c5244a21 | |
chzyer | 62c6fe6193 | |
chzyer | c2d777f687 | |
aktungmak | 47d43db0fd | |
Jiří Setnička | cffdf641d1 | |
chzyer | dc578a10ae | |
chzyer | f83f3269ca | |
chzyer | 4f0ff8f850 | |
chzyer | d478c186fd | |
chzyer | 683bf8ff7c | |
chzyer | 3717d7c383 | |
chzyer | b411b0f4b2 | |
chzyer | 03625fbce3 | |
Steven Oud | 92c174e5fb | |
chzyer | 64a71f22be | |
Ante Kresic | dc15d0f641 | |
Xavier Damman | 8fbe9eac1a | |
chzyer | dc5da28fbf | |
chzyer | 1e0917c739 | |
chzyer | fd07ffef1b | |
chzyer | 3ea5940c39 | |
chzyer | 52d8a65723 | |
chzyer | de49e7f118 | |
chzyer | d4c46a49e6 | |
Michal Pristas | e3e573aa21 | |
chzyer | f2a9cba613 | |
chzyer | 6cc043de37 | |
chzyer | e950f01ab4 | |
chzyer | 9d26a3bde6 | |
chzyer | 4606bfd979 | |
chzyer | d85c8b4802 | |
chzyer | a5bc4d464a | |
chzyer | 5652dc7c25 | |
chzyer | 218eb7fff6 | |
chzyer | 91ba4d48f0 | |
chzyer | 19657124c7 | |
chzyer | e5e328dcc7 | |
chzyer | b1b67f8632 | |
chzyer | 30b462e50b | |
chzyer | 1e409caaf3 | |
chzyer | bb5b4af6e7 | |
chzyer | 4e554e2dd0 | |
Chzyer | 402307d9c0 | |
招牌疯子 | 3138d3552b | |
chzyer | b57eccfd02 | |
Cheney | 14e9df7f3c | |
Cheney | bd7c8c6fbe | |
Cheney | 4ae9d7e0fd | |
Cheney | c4ec21b1c6 | |
Cheney | 867002449c | |
Chzyer | e9e6c35b06 | |
Xavier Damman | f445eb9c9d | |
Chzyer | 44ccc71d92 | |
Cheney | f1ecca38cc | |
Cheney | e967bfcedb | |
Chzyer | a0bb3f70e4 | |
Lu Guanqun | d45b1ac5fc | |
Chzyer | 2978660421 | |
Cheney | 8e340f3ee8 | |
Chzyer | 5aaa89df05 | |
Ben Darnell | c3db9c3189 | |
Cheney | 8181d5ed14 | |
Cheney | 15e7be4ac2 | |
Cheney | 6368045a0b | |
Chzyer | 01066143c6 | |
Cheney | 00e1a8e95a | |
Cheney | 21acaf90fd | |
Cheney | 07485bbd8f | |
Chzyer | d435bdb1ae | |
Cheney | a0730873ad | |
Cheney | 01cffb6cb2 | |
Cheney | f20f365652 | |
Cheney | 1aa557f19e | |
Cheney | 8853e17195 | |
Chzyer | cbab1f1566 | |
Dan Cripe | 7f88ba2640 | |
Dan Cripe | d0e806295b | |
Dan Cripe | e0fe5c6252 | |
Dan Cripe | f89cf57370 | |
Dan Cripe | cd2d269a73 | |
Chzyer | 94a70819f4 | |
Ryan Hileman | 05d3fbcc2a | |
Cheney | 76dfcd2e50 | |
Cheney | c36905cc67 | |
Cheney | 1a0e228994 | |
Chzyer | 6ae0191254 | |
Chris P | f1bc3fbc0e | |
Chris P | ccd7589339 | |
Cheney | 09b448ca0b | |
Cheney | fb8cb23199 | |
Cheney | c1adc97620 | |
Cheney | 839c0013a8 | |
Cheney | 71e9536f4b | |
Chzyer | ee4d466b62 | |
Pristáš Michal | 4bc3b1f4f4 | |
Cheney | 4dffd60960 | |
Cheney | 5660cc8cb6 | |
Cheney | db2e5eae91 | |
Cheney | 7a18498f5b | |
Cheney | 9364259fb1 | |
Cheney | 5ee706df9b | |
Cheney | e870ba12fd | |
Cheney | d59da2cf77 | |
Chzyer | 6ba1b6d3f5 | |
Chzyer | ffee92f4b7 | |
James Netherton | 46f410607f | |
Pristáš Michal | bcc2f0762c | |
Pristáš Michal | 30303e5637 | |
Pristáš Michal | 391c225c0b | |
Pristáš Michal | de44b28597 | |
Pristáš Michal | 1c411ac48c | |
Cheney | 77fbb66748 | |
Cheney | 733245997a | |
Cheney | c73eba9879 | |
Cheney | 50197e309d | |
Cheney | 86a7212964 | |
Cheney | aec0cc08d9 | |
Cheney | 9f55547f9b | |
Cheney | d0699b888b | |
Chzyer | 3647bb9124 | |
招牌疯子 | 492f37d9d4 | |
Cheney | 0f7efea2bc | |
Cheney | 7638a04c39 | |
Cheney | 55809b401d | |
Cheney | bfa8c1dfdb | |
Cheney | 4dc2ce7141 | |
Cheney | e89699832b | |
Cheney | d173bd5ae7 | |
Cheney | 86ae077418 | |
Chzyer | 38e5213cc8 | |
Cheney | 4d6d6c223f | |
Cheney | 879224ddc9 | |
Cheney | 79d1bf27b4 | |
Cheney | 390f0ebb6b | |
Cheney | 3ccecf626d | |
Cheney | 180b650b65 | |
Chzyer | 2dbff40748 | |
Cheney | f517af0910 | |
Cheney | bb60b8a58f | |
Cheney | 6eb29567f6 | |
Cheney | 9edb463230 | |
Cheney | d4826eb059 | |
Cheney | 03d201ab65 | |
Cheney | c814ccae9a | |
Cheney | 7537bea372 | |
Cheney | 8dc3117d78 | |
Cheney | 593678baa5 | |
Cheney | 092d0fe477 | |
Cheney | 6a53e6406c | |
Cheney | 03a55ea489 | |
Cheney | a81fb5db7a | |
Cheney | 7e432495e0 | |
Cheney | d4ceb57901 | |
Chzyer | 0801a4ad00 | |
Chzyer | 884fc46e08 | |
Cheney | bed7d058a7 | |
Cheney | a8eaa99c90 | |
Cheney | e2dc5c8314 | |
Cheney | 1ff4e718ab | |
Cheney | 75bbfa6d42 | |
Cheney | d07044cdb6 | |
Cheney | 04f86e9c53 | |
Cheney | 28ad744d6b | |
Cheney | 69871d9ae0 | |
Cheney | 6f64c527fe | |
Cheney | f179b24304 | |
Cheney | af66dc48f7 | |
Cheney | a904b314b8 | |
Cheney | 8fcf9da96c | |
Cheney | a659448259 | |
Cheney | 80af385185 | |
Cheney | bc96fada95 | |
Cheney | 0de649aeb5 | |
Cheney | 25447666a0 | |
Cheney | 3c4b5dd53d | |
Cheney | 87df30271f | |
Cheney | 2622435bfa | |
Cheney | 93a73361b8 | |
Cheney | 97dbc9e329 | |
Cheney | cf3342ccf3 | |
Cheney | f5bac25865 | |
Cheney | fa73762a9b | |
Cheney | 1766317571 | |
Cheney | 0c61a83a1e | |
Cheney | 20f1239f0f | |
Chzyer | 7edbf70643 | |
Cheney | adde40bb56 | |
Cheney | bfdaae1594 | |
Cheney | 9848c61567 | |
Cheney | 4ab9a96399 | |
Cheney | 2a02950868 | |
Cheney | 41404cd3f9 | |
Cheney | a7f498f047 | |
Cheney | 3b1cf6b8fb | |
Cheney | 3f23122fec | |
Cheney | c8f8ec4b96 | |
Cheney | f8c012aa53 | |
Cheney | c9aba7b858 | |
Cheney | 9c65cb7ccf | |
Cheney | e878807b59 | |
Cheney | 772978399e | |
Cheney | c16e43d258 | |
Cheney | 6642cc6506 | |
Chzyer | 8f9ae9433d | |
Cheney | 740e90a464 |
|
@ -0,0 +1 @@
|
||||||
|
.vscode/*
|
|
@ -0,0 +1,8 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.x
|
||||||
|
script:
|
||||||
|
- GOOS=windows go install git.internal/re/readline/example/...
|
||||||
|
- GOOS=linux go install git.internal/re/readline/example/...
|
||||||
|
- GOOS=darwin go install git.internal/re/readline/example/...
|
||||||
|
- go test -race -v
|
|
@ -0,0 +1,58 @@
|
||||||
|
# ChangeLog
|
||||||
|
|
||||||
|
### 1.4 - 2016-07-25
|
||||||
|
|
||||||
|
* [#60][60] Support dynamic autocompletion
|
||||||
|
* Fix ANSI parser on Windows
|
||||||
|
* Fix wrong column width in complete mode on Windows
|
||||||
|
* Remove dependent package "golang.org/x/crypto/ssh/terminal"
|
||||||
|
|
||||||
|
### 1.3 - 2016-05-09
|
||||||
|
|
||||||
|
* [#38][38] add SetChildren for prefix completer interface
|
||||||
|
* [#42][42] improve multiple lines compatibility
|
||||||
|
* [#43][43] remove sub-package(runes) for gopkg compatibility
|
||||||
|
* [#46][46] Auto complete with space prefixed line
|
||||||
|
* [#48][48] support suspend process (ctrl+Z)
|
||||||
|
* [#49][49] fix bug that check equals with previous command
|
||||||
|
* [#53][53] Fix bug which causes integer divide by zero panicking when input buffer is empty
|
||||||
|
|
||||||
|
### 1.2 - 2016-03-05
|
||||||
|
|
||||||
|
* 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
|
||||||
|
* [#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://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.
|
||||||
|
* 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.
|
||||||
|
* [#33][33], vim mode now support `c`, `d`, `x (delete character)`, `r (replace character)`
|
||||||
|
|
||||||
|
### 1.1 - 2015-11-20
|
||||||
|
|
||||||
|
* [#12][12] Add support for key `<Delete>`/`<Home>`/`<End>`
|
||||||
|
* Only enter raw mode as needed (calling `Readline()`), program will receive signal(e.g. Ctrl+C) if not interact with `readline`.
|
||||||
|
* Bugs fixed for `PrefixCompleter`
|
||||||
|
* Press `Ctrl+D` in empty line will cause `io.EOF` in error, Press `Ctrl+C` in anytime will cause `ErrInterrupt` instead of `io.EOF`, this will privodes a shell-like user experience.
|
||||||
|
* Customable Interrupt/EOF prompt in `Config`
|
||||||
|
* [#17][17] Change atomic package to use 32bit function to let it runnable on arm 32bit devices
|
||||||
|
* Provides a new password user experience(`readline.ReadPasswordEx()`).
|
||||||
|
|
||||||
|
### 1.0 - 2015-10-14
|
||||||
|
|
||||||
|
* Initial public release.
|
||||||
|
|
||||||
|
[12]: https://git.internal/re/readline/pull/12
|
||||||
|
[17]: https://git.internal/re/readline/pull/17
|
||||||
|
[23]: https://git.internal/re/readline/pull/23
|
||||||
|
[27]: https://git.internal/re/readline/pull/27
|
||||||
|
[28]: https://git.internal/re/readline/pull/28
|
||||||
|
[33]: https://git.internal/re/readline/pull/33
|
||||||
|
[38]: https://git.internal/re/readline/pull/38
|
||||||
|
[42]: https://git.internal/re/readline/pull/42
|
||||||
|
[43]: https://git.internal/re/readline/pull/43
|
||||||
|
[46]: https://git.internal/re/readline/pull/46
|
||||||
|
[48]: https://git.internal/re/readline/pull/48
|
||||||
|
[49]: https://git.internal/re/readline/pull/49
|
||||||
|
[53]: https://git.internal/re/readline/pull/53
|
||||||
|
[60]: https://git.internal/re/readline/pull/60
|
116
README.md
116
README.md
|
@ -1,2 +1,114 @@
|
||||||
# readline
|
[![Build Status](https://travis-ci.org/chzyer/readline.svg?branch=master)](https://travis-ci.org/chzyer/readline)
|
||||||
A pure go implementation for gnu readline.
|
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE.md)
|
||||||
|
[![Version](https://img.shields.io/github/tag/chzyer/readline.svg)](https://git.internal/re/readline/releases)
|
||||||
|
[![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/sponsors.svg)](#sponsors)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/chzyer/readline/assets/logo.png" />
|
||||||
|
<a href="https://asciinema.org/a/32oseof9mkilg7t7d4780qt4m" target="_blank"><img src="https://asciinema.org/a/32oseof9mkilg7t7d4780qt4m.png" width="654"/></a>
|
||||||
|
<img src="https://raw.githubusercontent.com/chzyer/readline/assets/logo_f.png" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
A powerful readline library in `Linux` `macOS` `Windows` `Solaris` `AIX`
|
||||||
|
|
||||||
|
## Guide
|
||||||
|
|
||||||
|
* [Demo](example/readline-demo/readline-demo.go)
|
||||||
|
* [Shortcut](doc/shortcut.md)
|
||||||
|
|
||||||
|
## Repos using readline
|
||||||
|
|
||||||
|
[![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)
|
||||||
|
[![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)
|
||||||
|
[![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)
|
||||||
|
[![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)
|
||||||
|
|
||||||
|
|
||||||
|
## Feedback
|
||||||
|
|
||||||
|
If you have any questions, please submit a github issue and any pull requests is welcomed :)
|
||||||
|
|
||||||
|
* [https://twitter.com/chzyer](https://twitter.com/chzyer)
|
||||||
|
* [http://weibo.com/2145262190](http://weibo.com/2145262190)
|
||||||
|
|
||||||
|
|
||||||
|
## Backers
|
||||||
|
|
||||||
|
Love Readline? Help me keep it alive by donating funds to cover project expenses!<br />
|
||||||
|
[[Become a backer](https://opencollective.com/readline#backer)]
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/readline/backer/0/website" target="_blank"><img src="https://opencollective.com/readline/backer/0/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/1/website" target="_blank"><img src="https://opencollective.com/readline/backer/1/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/2/website" target="_blank"><img src="https://opencollective.com/readline/backer/2/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/3/website" target="_blank"><img src="https://opencollective.com/readline/backer/3/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/4/website" target="_blank"><img src="https://opencollective.com/readline/backer/4/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/5/website" target="_blank"><img src="https://opencollective.com/readline/backer/5/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/6/website" target="_blank"><img src="https://opencollective.com/readline/backer/6/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/7/website" target="_blank"><img src="https://opencollective.com/readline/backer/7/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/8/website" target="_blank"><img src="https://opencollective.com/readline/backer/8/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/9/website" target="_blank"><img src="https://opencollective.com/readline/backer/9/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/10/website" target="_blank"><img src="https://opencollective.com/readline/backer/10/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/11/website" target="_blank"><img src="https://opencollective.com/readline/backer/11/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/12/website" target="_blank"><img src="https://opencollective.com/readline/backer/12/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/13/website" target="_blank"><img src="https://opencollective.com/readline/backer/13/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/14/website" target="_blank"><img src="https://opencollective.com/readline/backer/14/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/15/website" target="_blank"><img src="https://opencollective.com/readline/backer/15/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/16/website" target="_blank"><img src="https://opencollective.com/readline/backer/16/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/17/website" target="_blank"><img src="https://opencollective.com/readline/backer/17/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/18/website" target="_blank"><img src="https://opencollective.com/readline/backer/18/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/19/website" target="_blank"><img src="https://opencollective.com/readline/backer/19/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/20/website" target="_blank"><img src="https://opencollective.com/readline/backer/20/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/21/website" target="_blank"><img src="https://opencollective.com/readline/backer/21/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/22/website" target="_blank"><img src="https://opencollective.com/readline/backer/22/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/23/website" target="_blank"><img src="https://opencollective.com/readline/backer/23/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/24/website" target="_blank"><img src="https://opencollective.com/readline/backer/24/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/25/website" target="_blank"><img src="https://opencollective.com/readline/backer/25/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/26/website" target="_blank"><img src="https://opencollective.com/readline/backer/26/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/27/website" target="_blank"><img src="https://opencollective.com/readline/backer/27/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/28/website" target="_blank"><img src="https://opencollective.com/readline/backer/28/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/backer/29/website" target="_blank"><img src="https://opencollective.com/readline/backer/29/avatar.svg"></a>
|
||||||
|
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
Become a sponsor and get your logo here on our Github page. [[Become a sponsor](https://opencollective.com/readline#sponsor)]
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/0/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/0/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/1/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/1/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/2/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/2/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/3/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/3/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/4/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/4/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/5/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/5/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/6/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/6/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/7/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/7/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/8/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/8/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/9/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/9/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/10/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/10/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/11/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/11/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/12/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/12/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/13/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/13/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/14/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/14/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/15/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/15/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/16/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/16/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/17/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/17/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/18/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/18/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/19/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/19/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/20/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/20/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/21/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/21/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/22/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/22/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/23/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/23/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/24/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/24/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/25/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/25/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/26/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/26/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/27/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/27/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/28/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/28/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/readline/sponsor/29/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/29/avatar.svg"></a>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,249 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unicode/utf8"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = uint16(0)
|
||||||
|
COLOR_FBLUE = 0x0001
|
||||||
|
COLOR_FGREEN = 0x0002
|
||||||
|
COLOR_FRED = 0x0004
|
||||||
|
COLOR_FINTENSITY = 0x0008
|
||||||
|
|
||||||
|
COLOR_BBLUE = 0x0010
|
||||||
|
COLOR_BGREEN = 0x0020
|
||||||
|
COLOR_BRED = 0x0040
|
||||||
|
COLOR_BINTENSITY = 0x0080
|
||||||
|
|
||||||
|
COMMON_LVB_UNDERSCORE = 0x8000
|
||||||
|
COMMON_LVB_BOLD = 0x0007
|
||||||
|
)
|
||||||
|
|
||||||
|
var ColorTableFg = []word{
|
||||||
|
0, // 30: Black
|
||||||
|
COLOR_FRED, // 31: Red
|
||||||
|
COLOR_FGREEN, // 32: Green
|
||||||
|
COLOR_FRED | COLOR_FGREEN, // 33: Yellow
|
||||||
|
COLOR_FBLUE, // 34: Blue
|
||||||
|
COLOR_FRED | COLOR_FBLUE, // 35: Magenta
|
||||||
|
COLOR_FGREEN | COLOR_FBLUE, // 36: Cyan
|
||||||
|
COLOR_FRED | COLOR_FBLUE | COLOR_FGREEN, // 37: White
|
||||||
|
}
|
||||||
|
|
||||||
|
var ColorTableBg = []word{
|
||||||
|
0, // 40: Black
|
||||||
|
COLOR_BRED, // 41: Red
|
||||||
|
COLOR_BGREEN, // 42: Green
|
||||||
|
COLOR_BRED | COLOR_BGREEN, // 43: Yellow
|
||||||
|
COLOR_BBLUE, // 44: Blue
|
||||||
|
COLOR_BRED | COLOR_BBLUE, // 45: Magenta
|
||||||
|
COLOR_BGREEN | COLOR_BBLUE, // 46: Cyan
|
||||||
|
COLOR_BRED | COLOR_BBLUE | COLOR_BGREEN, // 47: White
|
||||||
|
}
|
||||||
|
|
||||||
|
type ANSIWriter struct {
|
||||||
|
target io.Writer
|
||||||
|
wg sync.WaitGroup
|
||||||
|
ctx *ANSIWriterCtx
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewANSIWriter(w io.Writer) *ANSIWriter {
|
||||||
|
a := &ANSIWriter{
|
||||||
|
target: w,
|
||||||
|
ctx: NewANSIWriterCtx(w),
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ANSIWriter) Close() error {
|
||||||
|
a.wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ANSIWriterCtx struct {
|
||||||
|
isEsc bool
|
||||||
|
isEscSeq bool
|
||||||
|
arg []string
|
||||||
|
target *bufio.Writer
|
||||||
|
wantFlush bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewANSIWriterCtx(target io.Writer) *ANSIWriterCtx {
|
||||||
|
return &ANSIWriterCtx{
|
||||||
|
target: bufio.NewWriter(target),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ANSIWriterCtx) Flush() {
|
||||||
|
a.target.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ANSIWriterCtx) process(r rune) bool {
|
||||||
|
if a.wantFlush {
|
||||||
|
if r == 0 || r == CharEsc {
|
||||||
|
a.wantFlush = false
|
||||||
|
a.target.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.isEscSeq {
|
||||||
|
a.isEscSeq = a.ioloopEscSeq(a.target, r, &a.arg)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r {
|
||||||
|
case CharEsc:
|
||||||
|
a.isEsc = true
|
||||||
|
case '[':
|
||||||
|
if a.isEsc {
|
||||||
|
a.arg = nil
|
||||||
|
a.isEscSeq = true
|
||||||
|
a.isEsc = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
a.target.WriteRune(r)
|
||||||
|
a.wantFlush = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ANSIWriterCtx) ioloopEscSeq(w *bufio.Writer, r rune, argptr *[]string) bool {
|
||||||
|
arg := *argptr
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if r >= 'A' && r <= 'D' {
|
||||||
|
count := short(GetInt(arg, 1))
|
||||||
|
info, err := GetConsoleScreenBufferInfo()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch r {
|
||||||
|
case 'A': // up
|
||||||
|
info.dwCursorPosition.y -= count
|
||||||
|
case 'B': // down
|
||||||
|
info.dwCursorPosition.y += count
|
||||||
|
case 'C': // right
|
||||||
|
info.dwCursorPosition.x += count
|
||||||
|
case 'D': // left
|
||||||
|
info.dwCursorPosition.x -= count
|
||||||
|
}
|
||||||
|
SetConsoleCursorPosition(&info.dwCursorPosition)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r {
|
||||||
|
case 'J':
|
||||||
|
killLines()
|
||||||
|
case 'K':
|
||||||
|
eraseLine()
|
||||||
|
case 'm':
|
||||||
|
color := word(0)
|
||||||
|
for _, item := range arg {
|
||||||
|
var c int
|
||||||
|
c, err = strconv.Atoi(item)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteString("[" + strings.Join(arg, ";") + "m")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if c >= 30 && c < 40 {
|
||||||
|
color ^= COLOR_FINTENSITY
|
||||||
|
color |= ColorTableFg[c-30]
|
||||||
|
} else if c >= 40 && c < 50 {
|
||||||
|
color ^= COLOR_BINTENSITY
|
||||||
|
color |= ColorTableBg[c-40]
|
||||||
|
} else if c == 4 {
|
||||||
|
color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7]
|
||||||
|
} else if c == 1 {
|
||||||
|
color |= COMMON_LVB_BOLD | COLOR_FINTENSITY
|
||||||
|
} else { // unknown code treat as reset
|
||||||
|
color = ColorTableFg[7]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
kernel.SetConsoleTextAttribute(stdout, uintptr(color))
|
||||||
|
case '\007': // set title
|
||||||
|
case ';':
|
||||||
|
if len(arg) == 0 || arg[len(arg)-1] != "" {
|
||||||
|
arg = append(arg, "")
|
||||||
|
*argptr = arg
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
if len(arg) == 0 {
|
||||||
|
arg = append(arg, "")
|
||||||
|
}
|
||||||
|
arg[len(arg)-1] += string(r)
|
||||||
|
*argptr = arg
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
*argptr = nil
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ANSIWriter) Write(b []byte) (int, error) {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
|
||||||
|
off := 0
|
||||||
|
for len(b) > off {
|
||||||
|
r, size := utf8.DecodeRune(b[off:])
|
||||||
|
if size == 0 {
|
||||||
|
return off, io.ErrShortWrite
|
||||||
|
}
|
||||||
|
off += size
|
||||||
|
a.ctx.process(r)
|
||||||
|
}
|
||||||
|
a.ctx.Flush()
|
||||||
|
return off, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func killLines() error {
|
||||||
|
sbi, err := GetConsoleScreenBufferInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
size := (sbi.dwCursorPosition.y - sbi.dwSize.y) * sbi.dwSize.x
|
||||||
|
size += sbi.dwCursorPosition.x
|
||||||
|
|
||||||
|
var written int
|
||||||
|
kernel.FillConsoleOutputAttribute(stdout, uintptr(ColorTableFg[7]),
|
||||||
|
uintptr(size),
|
||||||
|
sbi.dwCursorPosition.ptr(),
|
||||||
|
uintptr(unsafe.Pointer(&written)),
|
||||||
|
)
|
||||||
|
return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
|
||||||
|
uintptr(size),
|
||||||
|
sbi.dwCursorPosition.ptr(),
|
||||||
|
uintptr(unsafe.Pointer(&written)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func eraseLine() error {
|
||||||
|
sbi, err := GetConsoleScreenBufferInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
size := sbi.dwSize.x
|
||||||
|
sbi.dwCursorPosition.x = 0
|
||||||
|
var written int
|
||||||
|
return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
|
||||||
|
uintptr(size),
|
||||||
|
sbi.dwCursorPosition.ptr(),
|
||||||
|
uintptr(unsafe.Pointer(&written)),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,285 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AutoCompleter interface {
|
||||||
|
// Readline will pass the whole line and current offset to it
|
||||||
|
// Completer need to pass all the candidates, and how long they shared the same characters in line
|
||||||
|
// Example:
|
||||||
|
// [go, git, git-shell, grep]
|
||||||
|
// Do("g", 1) => ["o", "it", "it-shell", "rep"], 1
|
||||||
|
// Do("gi", 2) => ["t", "t-shell"], 2
|
||||||
|
// Do("git", 3) => ["", "-shell"], 3
|
||||||
|
Do(line []rune, pos int) (newLine [][]rune, length int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabCompleter struct{}
|
||||||
|
|
||||||
|
func (t *TabCompleter) Do([]rune, int) ([][]rune, int) {
|
||||||
|
return [][]rune{[]rune("\t")}, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type opCompleter struct {
|
||||||
|
w io.Writer
|
||||||
|
op *Operation
|
||||||
|
width int
|
||||||
|
|
||||||
|
inCompleteMode bool
|
||||||
|
inSelectMode bool
|
||||||
|
candidate [][]rune
|
||||||
|
candidateSource []rune
|
||||||
|
candidateOff int
|
||||||
|
candidateChoise int
|
||||||
|
candidateColNum int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
|
||||||
|
return &opCompleter{
|
||||||
|
w: w,
|
||||||
|
op: op,
|
||||||
|
width: width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) doSelect() {
|
||||||
|
if len(o.candidate) == 1 {
|
||||||
|
o.op.buf.WriteRunes(o.candidate[0])
|
||||||
|
o.ExitCompleteMode(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o.nextCandidate(1)
|
||||||
|
o.CompleteRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) nextCandidate(i int) {
|
||||||
|
o.candidateChoise += i
|
||||||
|
o.candidateChoise = o.candidateChoise % len(o.candidate)
|
||||||
|
if o.candidateChoise < 0 {
|
||||||
|
o.candidateChoise = len(o.candidate) + o.candidateChoise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) OnComplete() bool {
|
||||||
|
if o.width == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if o.IsInCompleteSelectMode() {
|
||||||
|
o.doSelect()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := o.op.buf
|
||||||
|
rs := buf.Runes()
|
||||||
|
|
||||||
|
if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) {
|
||||||
|
o.EnterCompleteSelectMode()
|
||||||
|
o.doSelect()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
o.ExitCompleteSelectMode()
|
||||||
|
o.candidateSource = rs
|
||||||
|
newLines, offset := o.op.cfg.AutoComplete.Do(rs, buf.idx)
|
||||||
|
if len(newLines) == 0 {
|
||||||
|
o.ExitCompleteMode(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// only Aggregate candidates in non-complete mode
|
||||||
|
if !o.IsInCompleteMode() {
|
||||||
|
if len(newLines) == 1 {
|
||||||
|
buf.WriteRunes(newLines[0])
|
||||||
|
o.ExitCompleteMode(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
same, size := runes.Aggregate(newLines)
|
||||||
|
if size > 0 {
|
||||||
|
buf.WriteRunes(same)
|
||||||
|
o.ExitCompleteMode(false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
o.EnterCompleteMode(offset, newLines)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) IsInCompleteSelectMode() bool {
|
||||||
|
return o.inSelectMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) IsInCompleteMode() bool {
|
||||||
|
return o.inCompleteMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) HandleCompleteSelect(r rune) bool {
|
||||||
|
next := true
|
||||||
|
switch r {
|
||||||
|
case CharEnter, CharCtrlJ:
|
||||||
|
next = false
|
||||||
|
o.op.buf.WriteRunes(o.op.candidate[o.op.candidateChoise])
|
||||||
|
o.ExitCompleteMode(false)
|
||||||
|
case CharLineStart:
|
||||||
|
num := o.candidateChoise % o.candidateColNum
|
||||||
|
o.nextCandidate(-num)
|
||||||
|
case CharLineEnd:
|
||||||
|
num := o.candidateColNum - o.candidateChoise%o.candidateColNum - 1
|
||||||
|
o.candidateChoise += num
|
||||||
|
if o.candidateChoise >= len(o.candidate) {
|
||||||
|
o.candidateChoise = len(o.candidate) - 1
|
||||||
|
}
|
||||||
|
case CharBackspace:
|
||||||
|
o.ExitCompleteSelectMode()
|
||||||
|
next = false
|
||||||
|
case CharTab, CharForward:
|
||||||
|
o.doSelect()
|
||||||
|
case CharBell, CharInterrupt:
|
||||||
|
o.ExitCompleteMode(true)
|
||||||
|
next = false
|
||||||
|
case CharNext:
|
||||||
|
tmpChoise := o.candidateChoise + o.candidateColNum
|
||||||
|
if tmpChoise >= o.getMatrixSize() {
|
||||||
|
tmpChoise -= o.getMatrixSize()
|
||||||
|
} else if tmpChoise >= len(o.candidate) {
|
||||||
|
tmpChoise += o.candidateColNum
|
||||||
|
tmpChoise -= o.getMatrixSize()
|
||||||
|
}
|
||||||
|
o.candidateChoise = tmpChoise
|
||||||
|
case CharBackward:
|
||||||
|
o.nextCandidate(-1)
|
||||||
|
case CharPrev:
|
||||||
|
tmpChoise := o.candidateChoise - o.candidateColNum
|
||||||
|
if tmpChoise < 0 {
|
||||||
|
tmpChoise += o.getMatrixSize()
|
||||||
|
if tmpChoise >= len(o.candidate) {
|
||||||
|
tmpChoise -= o.candidateColNum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o.candidateChoise = tmpChoise
|
||||||
|
default:
|
||||||
|
next = false
|
||||||
|
o.ExitCompleteSelectMode()
|
||||||
|
}
|
||||||
|
if next {
|
||||||
|
o.CompleteRefresh()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) getMatrixSize() int {
|
||||||
|
line := len(o.candidate) / o.candidateColNum
|
||||||
|
if len(o.candidate)%o.candidateColNum != 0 {
|
||||||
|
line++
|
||||||
|
}
|
||||||
|
return line * o.candidateColNum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) OnWidthChange(newWidth int) {
|
||||||
|
o.width = newWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) CompleteRefresh() {
|
||||||
|
if !o.inCompleteMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lineCnt := o.op.buf.CursorLineCount()
|
||||||
|
colWidth := 0
|
||||||
|
for _, c := range o.candidate {
|
||||||
|
w := runes.WidthAll(c)
|
||||||
|
if w > colWidth {
|
||||||
|
colWidth = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
colWidth += o.candidateOff + 1
|
||||||
|
same := o.op.buf.RuneSlice(-o.candidateOff)
|
||||||
|
|
||||||
|
// -1 to avoid reach the end of line
|
||||||
|
width := o.width - 1
|
||||||
|
colNum := width / colWidth
|
||||||
|
if colNum != 0 {
|
||||||
|
colWidth += (width - (colWidth * colNum)) / colNum
|
||||||
|
}
|
||||||
|
|
||||||
|
o.candidateColNum = colNum
|
||||||
|
buf := bufio.NewWriter(o.w)
|
||||||
|
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
|
||||||
|
|
||||||
|
colIdx := 0
|
||||||
|
lines := 1
|
||||||
|
buf.WriteString("\033[J")
|
||||||
|
for idx, c := range o.candidate {
|
||||||
|
inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode()
|
||||||
|
if inSelect {
|
||||||
|
buf.WriteString("\033[30;47m")
|
||||||
|
}
|
||||||
|
buf.WriteString(string(same))
|
||||||
|
buf.WriteString(string(c))
|
||||||
|
buf.Write(bytes.Repeat([]byte(" "), colWidth-runes.WidthAll(c)-runes.WidthAll(same)))
|
||||||
|
|
||||||
|
if inSelect {
|
||||||
|
buf.WriteString("\033[0m")
|
||||||
|
}
|
||||||
|
|
||||||
|
colIdx++
|
||||||
|
if colIdx == colNum {
|
||||||
|
buf.WriteString("\n")
|
||||||
|
lines++
|
||||||
|
colIdx = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move back
|
||||||
|
fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines)
|
||||||
|
fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen())
|
||||||
|
buf.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) aggCandidate(candidate [][]rune) int {
|
||||||
|
offset := 0
|
||||||
|
for i := 0; i < len(candidate[0]); i++ {
|
||||||
|
for j := 0; j < len(candidate)-1; j++ {
|
||||||
|
if i > len(candidate[j]) {
|
||||||
|
goto aggregate
|
||||||
|
}
|
||||||
|
if candidate[j][i] != candidate[j+1][i] {
|
||||||
|
goto aggregate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset = i
|
||||||
|
}
|
||||||
|
aggregate:
|
||||||
|
return offset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) EnterCompleteSelectMode() {
|
||||||
|
o.inSelectMode = true
|
||||||
|
o.candidateChoise = -1
|
||||||
|
o.CompleteRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) {
|
||||||
|
o.inCompleteMode = true
|
||||||
|
o.candidate = candidate
|
||||||
|
o.candidateOff = offset
|
||||||
|
o.CompleteRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) ExitCompleteSelectMode() {
|
||||||
|
o.inSelectMode = false
|
||||||
|
o.candidate = nil
|
||||||
|
o.candidateChoise = -1
|
||||||
|
o.candidateOff = -1
|
||||||
|
o.candidateSource = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opCompleter) ExitCompleteMode(revent bool) {
|
||||||
|
o.inCompleteMode = false
|
||||||
|
o.ExitCompleteSelectMode()
|
||||||
|
}
|
|
@ -0,0 +1,165 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Caller type for dynamic completion
|
||||||
|
type DynamicCompleteFunc func(string) []string
|
||||||
|
|
||||||
|
type PrefixCompleterInterface interface {
|
||||||
|
Print(prefix string, level int, buf *bytes.Buffer)
|
||||||
|
Do(line []rune, pos int) (newLine [][]rune, length int)
|
||||||
|
GetName() []rune
|
||||||
|
GetChildren() []PrefixCompleterInterface
|
||||||
|
SetChildren(children []PrefixCompleterInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DynamicPrefixCompleterInterface interface {
|
||||||
|
PrefixCompleterInterface
|
||||||
|
IsDynamic() bool
|
||||||
|
GetDynamicNames(line []rune) [][]rune
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrefixCompleter struct {
|
||||||
|
Name []rune
|
||||||
|
Dynamic bool
|
||||||
|
Callback DynamicCompleteFunc
|
||||||
|
Children []PrefixCompleterInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrefixCompleter) Tree(prefix string) string {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
p.Print(prefix, 0, buf)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Print(p PrefixCompleterInterface, prefix string, level int, buf *bytes.Buffer) {
|
||||||
|
if strings.TrimSpace(string(p.GetName())) != "" {
|
||||||
|
buf.WriteString(prefix)
|
||||||
|
if level > 0 {
|
||||||
|
buf.WriteString("├")
|
||||||
|
buf.WriteString(strings.Repeat("─", (level*4)-2))
|
||||||
|
buf.WriteString(" ")
|
||||||
|
}
|
||||||
|
buf.WriteString(string(p.GetName()) + "\n")
|
||||||
|
level++
|
||||||
|
}
|
||||||
|
for _, ch := range p.GetChildren() {
|
||||||
|
ch.Print(prefix, level, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrefixCompleter) Print(prefix string, level int, buf *bytes.Buffer) {
|
||||||
|
Print(p, prefix, level, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrefixCompleter) IsDynamic() bool {
|
||||||
|
return p.Dynamic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrefixCompleter) GetName() []rune {
|
||||||
|
return p.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrefixCompleter) GetDynamicNames(line []rune) [][]rune {
|
||||||
|
var names = [][]rune{}
|
||||||
|
for _, name := range p.Callback(string(line)) {
|
||||||
|
names = append(names, []rune(name+" "))
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrefixCompleter) GetChildren() []PrefixCompleterInterface {
|
||||||
|
return p.Children
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrefixCompleter) SetChildren(children []PrefixCompleterInterface) {
|
||||||
|
p.Children = children
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPrefixCompleter(pc ...PrefixCompleterInterface) *PrefixCompleter {
|
||||||
|
return PcItem("", pc...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PcItem(name string, pc ...PrefixCompleterInterface) *PrefixCompleter {
|
||||||
|
name += " "
|
||||||
|
return &PrefixCompleter{
|
||||||
|
Name: []rune(name),
|
||||||
|
Dynamic: false,
|
||||||
|
Children: pc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PcItemDynamic(callback DynamicCompleteFunc, pc ...PrefixCompleterInterface) *PrefixCompleter {
|
||||||
|
return &PrefixCompleter{
|
||||||
|
Callback: callback,
|
||||||
|
Dynamic: true,
|
||||||
|
Children: pc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) {
|
||||||
|
return doInternal(p, line, pos, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Do(p PrefixCompleterInterface, line []rune, pos int) (newLine [][]rune, offset int) {
|
||||||
|
return doInternal(p, line, pos, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doInternal(p PrefixCompleterInterface, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) {
|
||||||
|
line = runes.TrimSpaceLeft(line[:pos])
|
||||||
|
goNext := false
|
||||||
|
var lineCompleter PrefixCompleterInterface
|
||||||
|
for _, child := range p.GetChildren() {
|
||||||
|
childNames := make([][]rune, 1)
|
||||||
|
|
||||||
|
childDynamic, ok := child.(DynamicPrefixCompleterInterface)
|
||||||
|
if ok && childDynamic.IsDynamic() {
|
||||||
|
childNames = childDynamic.GetDynamicNames(origLine)
|
||||||
|
} else {
|
||||||
|
childNames[0] = child.GetName()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, childName := range childNames {
|
||||||
|
if len(line) >= len(childName) {
|
||||||
|
if runes.HasPrefix(line, childName) {
|
||||||
|
if len(line) == len(childName) {
|
||||||
|
newLine = append(newLine, []rune{' '})
|
||||||
|
} else {
|
||||||
|
newLine = append(newLine, childName)
|
||||||
|
}
|
||||||
|
offset = len(childName)
|
||||||
|
lineCompleter = child
|
||||||
|
goNext = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if runes.HasPrefix(childName, line) {
|
||||||
|
newLine = append(newLine, childName[len(line):])
|
||||||
|
offset = len(line)
|
||||||
|
lineCompleter = child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newLine) != 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpLine := make([]rune, 0, len(line))
|
||||||
|
for i := offset; i < len(line); i++ {
|
||||||
|
if line[i] == ' ' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpLine = append(tmpLine, line[i:]...)
|
||||||
|
return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
if goNext {
|
||||||
|
return doInternal(lineCompleter, nil, 0, origLine)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
type SegmentCompleter interface {
|
||||||
|
// a
|
||||||
|
// |- a1
|
||||||
|
// |--- a11
|
||||||
|
// |- a2
|
||||||
|
// b
|
||||||
|
// input:
|
||||||
|
// DoTree([], 0) [a, b]
|
||||||
|
// DoTree([a], 1) [a]
|
||||||
|
// DoTree([a, ], 0) [a1, a2]
|
||||||
|
// DoTree([a, a], 1) [a1, a2]
|
||||||
|
// DoTree([a, a1], 2) [a1]
|
||||||
|
// DoTree([a, a1, ], 0) [a11]
|
||||||
|
// DoTree([a, a1, a], 1) [a11]
|
||||||
|
DoSegment([][]rune, int) [][]rune
|
||||||
|
}
|
||||||
|
|
||||||
|
type dumpSegmentCompleter struct {
|
||||||
|
f func([][]rune, int) [][]rune
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dumpSegmentCompleter) DoSegment(segment [][]rune, n int) [][]rune {
|
||||||
|
return d.f(segment, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SegmentFunc(f func([][]rune, int) [][]rune) AutoCompleter {
|
||||||
|
return &SegmentComplete{&dumpSegmentCompleter{f}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SegmentAutoComplete(completer SegmentCompleter) *SegmentComplete {
|
||||||
|
return &SegmentComplete{
|
||||||
|
SegmentCompleter: completer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SegmentComplete struct {
|
||||||
|
SegmentCompleter
|
||||||
|
}
|
||||||
|
|
||||||
|
func RetSegment(segments [][]rune, cands [][]rune, idx int) ([][]rune, int) {
|
||||||
|
ret := make([][]rune, 0, len(cands))
|
||||||
|
lastSegment := segments[len(segments)-1]
|
||||||
|
for _, cand := range cands {
|
||||||
|
if !runes.HasPrefix(cand, lastSegment) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret = append(ret, cand[len(lastSegment):])
|
||||||
|
}
|
||||||
|
return ret, idx
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitSegment(line []rune, pos int) ([][]rune, int) {
|
||||||
|
segs := [][]rune{}
|
||||||
|
lastIdx := -1
|
||||||
|
line = line[:pos]
|
||||||
|
pos = 0
|
||||||
|
for idx, l := range line {
|
||||||
|
if l == ' ' {
|
||||||
|
pos = 0
|
||||||
|
segs = append(segs, line[lastIdx+1:idx])
|
||||||
|
lastIdx = idx
|
||||||
|
} else {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segs = append(segs, line[lastIdx+1:])
|
||||||
|
return segs, pos
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SegmentComplete) Do(line []rune, pos int) (newLine [][]rune, offset int) {
|
||||||
|
|
||||||
|
segment, idx := SplitSegment(line, pos)
|
||||||
|
|
||||||
|
cands := c.DoSegment(segment, idx)
|
||||||
|
newLine, offset = RetSegment(segment, cands, idx)
|
||||||
|
for idx := range newLine {
|
||||||
|
newLine[idx] = append(newLine[idx], ' ')
|
||||||
|
}
|
||||||
|
return newLine, offset
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/chzyer/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rs(s [][]rune) []string {
|
||||||
|
ret := make([]string, len(s))
|
||||||
|
for idx, ss := range s {
|
||||||
|
ret[idx] = string(ss)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func sr(s ...string) [][]rune {
|
||||||
|
ret := make([][]rune, len(s))
|
||||||
|
for idx, ss := range s {
|
||||||
|
ret[idx] = []rune(ss)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetSegment(t *testing.T) {
|
||||||
|
defer test.New(t)
|
||||||
|
// a
|
||||||
|
// |- a1
|
||||||
|
// |--- a11
|
||||||
|
// |--- a12
|
||||||
|
// |- a2
|
||||||
|
// |--- a21
|
||||||
|
// b
|
||||||
|
// add
|
||||||
|
// adddomain
|
||||||
|
ret := []struct {
|
||||||
|
Segments [][]rune
|
||||||
|
Cands [][]rune
|
||||||
|
idx int
|
||||||
|
Ret [][]rune
|
||||||
|
pos int
|
||||||
|
}{
|
||||||
|
{sr(""), sr("a", "b", "add", "adddomain"), 0, sr("a", "b", "add", "adddomain"), 0},
|
||||||
|
{sr("a"), sr("a", "add", "adddomain"), 1, sr("", "dd", "dddomain"), 1},
|
||||||
|
{sr("a", ""), sr("a1", "a2"), 0, sr("a1", "a2"), 0},
|
||||||
|
{sr("a", "a"), sr("a1", "a2"), 1, sr("1", "2"), 1},
|
||||||
|
{sr("a", "a1"), sr("a1"), 2, sr(""), 2},
|
||||||
|
{sr("add"), sr("add", "adddomain"), 2, sr("", "domain"), 2},
|
||||||
|
}
|
||||||
|
for idx, r := range ret {
|
||||||
|
ret, pos := RetSegment(r.Segments, r.Cands, r.idx)
|
||||||
|
test.Equal(ret, r.Ret, fmt.Errorf("%v", idx))
|
||||||
|
test.Equal(pos, r.pos, fmt.Errorf("%v", idx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitSegment(t *testing.T) {
|
||||||
|
defer test.New(t)
|
||||||
|
// a
|
||||||
|
// |- a1
|
||||||
|
// |--- a11
|
||||||
|
// |--- a12
|
||||||
|
// |- a2
|
||||||
|
// |--- a21
|
||||||
|
// b
|
||||||
|
ret := []struct {
|
||||||
|
Line string
|
||||||
|
Pos int
|
||||||
|
Segments [][]rune
|
||||||
|
Idx int
|
||||||
|
}{
|
||||||
|
{"", 0, sr(""), 0},
|
||||||
|
{"a", 1, sr("a"), 1},
|
||||||
|
{"a ", 2, sr("a", ""), 0},
|
||||||
|
{"a a", 3, sr("a", "a"), 1},
|
||||||
|
{"a a1", 4, sr("a", "a1"), 2},
|
||||||
|
{"a a1 ", 5, sr("a", "a1", ""), 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, r := range ret {
|
||||||
|
ret, idx := SplitSegment([]rune(r.Line), r.Pos)
|
||||||
|
test.Equal(rs(ret), rs(r.Segments), fmt.Errorf("%v", i))
|
||||||
|
test.Equal(idx, r.Idx, fmt.Errorf("%v", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tree struct {
|
||||||
|
Name string
|
||||||
|
Children []Tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSegmentCompleter(t *testing.T) {
|
||||||
|
defer test.New(t)
|
||||||
|
|
||||||
|
tree := Tree{"", []Tree{
|
||||||
|
{"a", []Tree{
|
||||||
|
{"a1", []Tree{
|
||||||
|
{"a11", nil},
|
||||||
|
{"a12", nil},
|
||||||
|
}},
|
||||||
|
{"a2", []Tree{
|
||||||
|
{"a21", nil},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
{"b", nil},
|
||||||
|
{"route", []Tree{
|
||||||
|
{"add", nil},
|
||||||
|
{"adddomain", nil},
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
s := SegmentFunc(func(ret [][]rune, n int) [][]rune {
|
||||||
|
tree := tree
|
||||||
|
main:
|
||||||
|
for level := 0; level < len(ret)-1; {
|
||||||
|
name := string(ret[level])
|
||||||
|
for _, t := range tree.Children {
|
||||||
|
if t.Name == name {
|
||||||
|
tree = t
|
||||||
|
level++
|
||||||
|
continue main
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = make([][]rune, len(tree.Children))
|
||||||
|
for idx, r := range tree.Children {
|
||||||
|
ret[idx] = []rune(r.Name)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
})
|
||||||
|
|
||||||
|
// a
|
||||||
|
// |- a1
|
||||||
|
// |--- a11
|
||||||
|
// |--- a12
|
||||||
|
// |- a2
|
||||||
|
// |--- a21
|
||||||
|
// b
|
||||||
|
ret := []struct {
|
||||||
|
Line string
|
||||||
|
Pos int
|
||||||
|
Ret [][]rune
|
||||||
|
Share int
|
||||||
|
}{
|
||||||
|
{"", 0, sr("a", "b", "route"), 0},
|
||||||
|
{"a", 1, sr(""), 1},
|
||||||
|
{"a ", 2, sr("a1", "a2"), 0},
|
||||||
|
{"a a", 3, sr("1", "2"), 1},
|
||||||
|
{"a a1", 4, sr(""), 2},
|
||||||
|
{"a a1 ", 5, sr("a11", "a12"), 0},
|
||||||
|
{"a a1 a", 6, sr("11", "12"), 1},
|
||||||
|
{"a a1 a1", 7, sr("1", "2"), 2},
|
||||||
|
{"a a1 a11", 8, sr(""), 3},
|
||||||
|
{"route add", 9, sr("", "domain"), 3},
|
||||||
|
}
|
||||||
|
for _, r := range ret {
|
||||||
|
for idx, rr := range r.Ret {
|
||||||
|
r.Ret[idx] = append(rr, ' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, r := range ret {
|
||||||
|
newLine, length := s.Do([]rune(r.Line), r.Pos)
|
||||||
|
test.Equal(rs(newLine), rs(r.Ret), fmt.Errorf("%v", i))
|
||||||
|
test.Equal(length, r.Share, fmt.Errorf("%v", i))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
## Readline Shortcut
|
||||||
|
|
||||||
|
`Meta`+`B` means press `Esc` and `n` separately.
|
||||||
|
Users can change that in terminal simulator(i.e. iTerm2) to `Alt`+`B`
|
||||||
|
Notice: `Meta`+`B` is equals with `Alt`+`B` in windows.
|
||||||
|
|
||||||
|
* Shortcut in normal mode
|
||||||
|
|
||||||
|
| Shortcut | Comment |
|
||||||
|
| ------------------ | --------------------------------- |
|
||||||
|
| `Ctrl`+`A` | Beginning of line |
|
||||||
|
| `Ctrl`+`B` / `←` | Backward one character |
|
||||||
|
| `Meta`+`B` | Backward one word |
|
||||||
|
| `Ctrl`+`C` | Send io.EOF |
|
||||||
|
| `Ctrl`+`D` | Delete one character |
|
||||||
|
| `Meta`+`D` | Delete one word |
|
||||||
|
| `Ctrl`+`E` | End of line |
|
||||||
|
| `Ctrl`+`F` / `→` | Forward one character |
|
||||||
|
| `Meta`+`F` | Forward one word |
|
||||||
|
| `Ctrl`+`G` | Cancel |
|
||||||
|
| `Ctrl`+`H` | Delete previous character |
|
||||||
|
| `Ctrl`+`I` / `Tab` | Command line completion |
|
||||||
|
| `Ctrl`+`J` | Line feed |
|
||||||
|
| `Ctrl`+`K` | Cut text to the end of line |
|
||||||
|
| `Ctrl`+`L` | Clear screen |
|
||||||
|
| `Ctrl`+`M` | Same as Enter key |
|
||||||
|
| `Ctrl`+`N` / `↓` | Next line (in history) |
|
||||||
|
| `Ctrl`+`P` / `↑` | Prev line (in history) |
|
||||||
|
| `Ctrl`+`R` | Search backwards in history |
|
||||||
|
| `Ctrl`+`S` | Search forwards in history |
|
||||||
|
| `Ctrl`+`T` | Transpose characters |
|
||||||
|
| `Meta`+`T` | Transpose words (TODO) |
|
||||||
|
| `Ctrl`+`U` | Cut text to the beginning of line |
|
||||||
|
| `Ctrl`+`W` | Cut previous word |
|
||||||
|
| `Backspace` | Delete previous character |
|
||||||
|
| `Meta`+`Backspace` | Cut previous word |
|
||||||
|
| `Enter` | Line feed |
|
||||||
|
|
||||||
|
|
||||||
|
* Shortcut in Search Mode (`Ctrl`+`S` or `Ctrl`+`r` to enter this mode)
|
||||||
|
|
||||||
|
| Shortcut | Comment |
|
||||||
|
| ----------------------- | --------------------------------------- |
|
||||||
|
| `Ctrl`+`S` | Search forwards in history |
|
||||||
|
| `Ctrl`+`R` | Search backwards in history |
|
||||||
|
| `Ctrl`+`C` / `Ctrl`+`G` | Exit Search Mode and revert the history |
|
||||||
|
| `Backspace` | Delete previous character |
|
||||||
|
| Other | Exit Search Mode |
|
||||||
|
|
||||||
|
* Shortcut in Complete Select Mode (double `Tab` to enter this mode)
|
||||||
|
|
||||||
|
| Shortcut | Comment |
|
||||||
|
| ----------------------- | ---------------------------------------- |
|
||||||
|
| `Ctrl`+`F` | Move Forward |
|
||||||
|
| `Ctrl`+`B` | Move Backward |
|
||||||
|
| `Ctrl`+`N` | Move to next line |
|
||||||
|
| `Ctrl`+`P` | Move to previous line |
|
||||||
|
| `Ctrl`+`A` | Move to the first candicate in current line |
|
||||||
|
| `Ctrl`+`E` | Move to the last candicate in current line |
|
||||||
|
| `Tab` / `Enter` | Use the word on cursor to complete |
|
||||||
|
| `Ctrl`+`C` / `Ctrl`+`G` | Exit Complete Select Mode |
|
||||||
|
| Other | Exit Complete Select Mode |
|
|
@ -0,0 +1,168 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.internal/re/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage(w io.Writer) {
|
||||||
|
io.WriteString(w, "commands:\n")
|
||||||
|
io.WriteString(w, completer.Tree(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function constructor - constructs new function for listing given directory
|
||||||
|
func listFiles(path string) func(string) []string {
|
||||||
|
return func(line string) []string {
|
||||||
|
names := make([]string, 0)
|
||||||
|
files, _ := ioutil.ReadDir(path)
|
||||||
|
for _, f := range files {
|
||||||
|
names = append(names, f.Name())
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var completer = readline.NewPrefixCompleter(
|
||||||
|
readline.PcItem("mode",
|
||||||
|
readline.PcItem("vi"),
|
||||||
|
readline.PcItem("emacs"),
|
||||||
|
),
|
||||||
|
readline.PcItem("login"),
|
||||||
|
readline.PcItem("say",
|
||||||
|
readline.PcItemDynamic(listFiles("./"),
|
||||||
|
readline.PcItem("with",
|
||||||
|
readline.PcItem("following"),
|
||||||
|
readline.PcItem("items"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
readline.PcItem("hello"),
|
||||||
|
readline.PcItem("bye"),
|
||||||
|
),
|
||||||
|
readline.PcItem("setprompt"),
|
||||||
|
readline.PcItem("setpassword"),
|
||||||
|
readline.PcItem("bye"),
|
||||||
|
readline.PcItem("help"),
|
||||||
|
readline.PcItem("go",
|
||||||
|
readline.PcItem("build", readline.PcItem("-o"), readline.PcItem("-v")),
|
||||||
|
readline.PcItem("install",
|
||||||
|
readline.PcItem("-v"),
|
||||||
|
readline.PcItem("-vv"),
|
||||||
|
readline.PcItem("-vvv"),
|
||||||
|
),
|
||||||
|
readline.PcItem("test"),
|
||||||
|
),
|
||||||
|
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() {
|
||||||
|
l, err := readline.NewEx(&readline.Config{
|
||||||
|
Prompt: "\033[31m»\033[0m ",
|
||||||
|
HistoryFile: "/tmp/readline.tmp",
|
||||||
|
AutoComplete: completer,
|
||||||
|
InterruptPrompt: "^C",
|
||||||
|
EOFPrompt: "exit",
|
||||||
|
|
||||||
|
HistorySearchFold: true,
|
||||||
|
FuncFilterInputRune: filterInput,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer l.Close()
|
||||||
|
l.CaptureExitSignal()
|
||||||
|
|
||||||
|
setPasswordCfg := l.GenPasswordConfig()
|
||||||
|
setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
|
||||||
|
l.SetPrompt(fmt.Sprintf("Enter password(%v): ", len(line)))
|
||||||
|
l.Refresh()
|
||||||
|
return nil, 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
log.SetOutput(l.Stderr())
|
||||||
|
for {
|
||||||
|
line, err := l.Readline()
|
||||||
|
if err == readline.ErrInterrupt {
|
||||||
|
if len(line) == 0 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "mode "):
|
||||||
|
switch line[5:] {
|
||||||
|
case "vi":
|
||||||
|
l.SetVimMode(true)
|
||||||
|
case "emacs":
|
||||||
|
l.SetVimMode(false)
|
||||||
|
default:
|
||||||
|
println("invalid mode:", line[5:])
|
||||||
|
}
|
||||||
|
case line == "mode":
|
||||||
|
if l.IsVimMode() {
|
||||||
|
println("current mode: vim")
|
||||||
|
} else {
|
||||||
|
println("current mode: emacs")
|
||||||
|
}
|
||||||
|
case line == "login":
|
||||||
|
pswd, err := l.ReadPassword("please enter your password: ")
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
println("you enter:", strconv.Quote(string(pswd)))
|
||||||
|
case line == "help":
|
||||||
|
usage(l.Stderr())
|
||||||
|
case line == "setpassword":
|
||||||
|
pswd, err := l.ReadPasswordWithConfig(setPasswordCfg)
|
||||||
|
if err == nil {
|
||||||
|
println("you set:", strconv.Quote(string(pswd)))
|
||||||
|
}
|
||||||
|
case strings.HasPrefix(line, "setprompt"):
|
||||||
|
if len(line) <= 10 {
|
||||||
|
log.Println("setprompt <prompt>")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
l.SetPrompt(line[10:])
|
||||||
|
case strings.HasPrefix(line, "say"):
|
||||||
|
line := strings.TrimSpace(line[3:])
|
||||||
|
if len(line) == 0 {
|
||||||
|
log.Println("say what?")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
for range time.Tick(time.Second) {
|
||||||
|
log.Println(line)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case line == "bye":
|
||||||
|
goto exit
|
||||||
|
case line == "sleep":
|
||||||
|
log.Println("sleep 4 second")
|
||||||
|
time.Sleep(4 * time.Second)
|
||||||
|
case line == "":
|
||||||
|
default:
|
||||||
|
log.Println("you said:", strconv.Quote(line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit:
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# readline-im
|
||||||
|
|
||||||
|
![readline-im](https://dl.dropboxusercontent.com/s/52hc7bo92g3pgi5/03F93B8D-9B4B-4D35-BBAA-22FBDAC7F299-26173-000164AA33980001.gif?dl=0)
|
|
@ -0,0 +1,60 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.internal/re/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rl, err := readline.NewEx(&readline.Config{
|
||||||
|
UniqueEditLine: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rl.Close()
|
||||||
|
|
||||||
|
rl.SetPrompt("username: ")
|
||||||
|
username, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl.ResetHistory()
|
||||||
|
log.SetOutput(rl.Stderr())
|
||||||
|
|
||||||
|
fmt.Fprintln(rl, "Hi,", username+"! My name is Dave.")
|
||||||
|
rl.SetPrompt(username + "> ")
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
rand.Seed(time.Now().Unix())
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Duration(rand.Intn(20)) * 100 * time.Millisecond):
|
||||||
|
case <-done:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
log.Println("Dave:", "hello")
|
||||||
|
}
|
||||||
|
log.Println("Dave:", "bye")
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
ln := rl.Line()
|
||||||
|
if ln.CanContinue() {
|
||||||
|
continue
|
||||||
|
} else if ln.CanBreak() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Println(username+":", ln.Line)
|
||||||
|
}
|
||||||
|
rl.Clean()
|
||||||
|
done <- struct{}{}
|
||||||
|
<-done
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.internal/re/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rl, err := readline.NewEx(&readline.Config{
|
||||||
|
Prompt: "> ",
|
||||||
|
HistoryFile: "/tmp/readline-multiline",
|
||||||
|
DisableAutoSaveHistory: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rl.Close()
|
||||||
|
|
||||||
|
var cmds []string
|
||||||
|
for {
|
||||||
|
line, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmds = append(cmds, line)
|
||||||
|
if !strings.HasSuffix(line, ";") {
|
||||||
|
rl.SetPrompt(">>> ")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd := strings.Join(cmds, " ")
|
||||||
|
cmds = cmds[:0]
|
||||||
|
rl.SetPrompt("> ")
|
||||||
|
rl.SaveHistory(cmd)
|
||||||
|
println(cmd)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
// This is a small example using readline to read a password
|
||||||
|
// and check it's strength while typing using the zxcvbn library.
|
||||||
|
// Depending on the strength the prompt is colored nicely to indicate strength.
|
||||||
|
//
|
||||||
|
// This file is licensed under the WTFPL:
|
||||||
|
//
|
||||||
|
// DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
// Version 2, December 2004
|
||||||
|
//
|
||||||
|
// Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
//
|
||||||
|
// Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
// copies of this license document, and changing it is allowed as long
|
||||||
|
// as the name is changed.
|
||||||
|
//
|
||||||
|
// DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
// TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
//
|
||||||
|
// 0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.internal/re/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Cyan = 36
|
||||||
|
Green = 32
|
||||||
|
Magenta = 35
|
||||||
|
Red = 31
|
||||||
|
Yellow = 33
|
||||||
|
BackgroundRed = 41
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reset sequence
|
||||||
|
var ColorResetEscape = "\033[0m"
|
||||||
|
|
||||||
|
// ColorResetEscape translates a ANSI color number to a color escape.
|
||||||
|
func ColorEscape(color int) string {
|
||||||
|
return fmt.Sprintf("\033[0;%dm", color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colorize the msg using ANSI color escapes
|
||||||
|
func Colorize(msg string, color int) string {
|
||||||
|
return ColorEscape(color) + msg + ColorResetEscape
|
||||||
|
}
|
||||||
|
|
||||||
|
func createStrengthPrompt(password []rune) string {
|
||||||
|
symbol, color := "", Red
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(password) <= 1:
|
||||||
|
symbol = "✗"
|
||||||
|
color = Red
|
||||||
|
case len(password) <= 3:
|
||||||
|
symbol = "⚡"
|
||||||
|
color = Magenta
|
||||||
|
case len(password) <= 5:
|
||||||
|
symbol = "⚠"
|
||||||
|
color = Yellow
|
||||||
|
default:
|
||||||
|
symbol = "✔"
|
||||||
|
color = Green
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := Colorize(symbol, color)
|
||||||
|
prompt += Colorize(" ENT", Cyan)
|
||||||
|
|
||||||
|
prompt += Colorize(" New Password: ", color)
|
||||||
|
return prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rl, err := readline.New("")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rl.Close()
|
||||||
|
|
||||||
|
setPasswordCfg := rl.GenPasswordConfig()
|
||||||
|
setPasswordCfg.SetListener(func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
|
||||||
|
rl.SetPrompt(createStrengthPrompt(line))
|
||||||
|
rl.Refresh()
|
||||||
|
return nil, 0, false
|
||||||
|
})
|
||||||
|
|
||||||
|
pswd, err := rl.ReadPasswordWithConfig(setPasswordCfg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Your password was:", string(pswd))
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "git.internal/re/readline"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := readline.DialRemote("tcp", ":12344"); err != nil {
|
||||||
|
println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.internal/re/readline"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := &readline.Config{
|
||||||
|
Prompt: "readline-remote: ",
|
||||||
|
}
|
||||||
|
handleFunc := func(rl *readline.Instance) {
|
||||||
|
for {
|
||||||
|
line, err := rl.Readline()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Fprintln(rl.Stdout(), "receive:"+line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := readline.ListenRemote("tcp", ":12344", cfg, handleFunc)
|
||||||
|
if err != nil {
|
||||||
|
println(err.Error())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -0,0 +1,330 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type hisItem struct {
|
||||||
|
Source []rune
|
||||||
|
Version int64
|
||||||
|
Tmp []rune
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *hisItem) Clean() {
|
||||||
|
h.Source = nil
|
||||||
|
h.Tmp = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type opHistory struct {
|
||||||
|
cfg *Config
|
||||||
|
history *list.List
|
||||||
|
historyVer int64
|
||||||
|
current *list.Element
|
||||||
|
fd *os.File
|
||||||
|
fdLock sync.Mutex
|
||||||
|
enable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOpHistory(cfg *Config) (o *opHistory) {
|
||||||
|
o = &opHistory{
|
||||||
|
cfg: cfg,
|
||||||
|
history: list.New(),
|
||||||
|
enable: true,
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Reset() {
|
||||||
|
o.history = list.New()
|
||||||
|
o.current = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) IsHistoryClosed() bool {
|
||||||
|
o.fdLock.Lock()
|
||||||
|
defer o.fdLock.Unlock()
|
||||||
|
return o.fd.Fd() == ^(uintptr(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Init() {
|
||||||
|
if o.IsHistoryClosed() {
|
||||||
|
o.initHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) initHistory() {
|
||||||
|
if o.cfg.HistoryFile != "" {
|
||||||
|
o.historyUpdatePath(o.cfg.HistoryFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only called by newOpHistory
|
||||||
|
func (o *opHistory) historyUpdatePath(path string) {
|
||||||
|
o.fdLock.Lock()
|
||||||
|
defer o.fdLock.Unlock()
|
||||||
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
o.fd = f
|
||||||
|
r := bufio.NewReader(o.fd)
|
||||||
|
total := 0
|
||||||
|
for ; ; total++ {
|
||||||
|
line, err := r.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// ignore the empty line
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
o.Push([]rune(line))
|
||||||
|
o.Compact()
|
||||||
|
}
|
||||||
|
if total > o.cfg.HistoryLimit {
|
||||||
|
o.rewriteLocked()
|
||||||
|
}
|
||||||
|
o.historyVer++
|
||||||
|
o.Push(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Compact() {
|
||||||
|
for o.history.Len() > o.cfg.HistoryLimit && o.history.Len() > 0 {
|
||||||
|
o.history.Remove(o.history.Front())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Rewrite() {
|
||||||
|
o.fdLock.Lock()
|
||||||
|
defer o.fdLock.Unlock()
|
||||||
|
o.rewriteLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) rewriteLocked() {
|
||||||
|
if o.cfg.HistoryFile == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile := o.cfg.HistoryFile + ".tmp"
|
||||||
|
fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bufio.NewWriter(fd)
|
||||||
|
for elem := o.history.Front(); elem != nil; elem = elem.Next() {
|
||||||
|
buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n")
|
||||||
|
}
|
||||||
|
buf.Flush()
|
||||||
|
|
||||||
|
// replace history file
|
||||||
|
if err = os.Rename(tmpFile, o.cfg.HistoryFile); err != nil {
|
||||||
|
fd.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.fd != nil {
|
||||||
|
o.fd.Close()
|
||||||
|
}
|
||||||
|
// fd is write only, just satisfy what we need.
|
||||||
|
o.fd = fd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Close() {
|
||||||
|
o.fdLock.Lock()
|
||||||
|
defer o.fdLock.Unlock()
|
||||||
|
if o.fd != nil {
|
||||||
|
o.fd.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) {
|
||||||
|
for elem := o.current; elem != nil; elem = elem.Prev() {
|
||||||
|
item := o.showItem(elem.Value)
|
||||||
|
if isNewSearch {
|
||||||
|
start += len(rs)
|
||||||
|
}
|
||||||
|
if elem == o.current {
|
||||||
|
if len(item) >= start {
|
||||||
|
item = item[:start]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx := runes.IndexAllBckEx(item, rs, o.cfg.HistorySearchFold)
|
||||||
|
if idx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return idx, elem
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) {
|
||||||
|
for elem := o.current; elem != nil; elem = elem.Next() {
|
||||||
|
item := o.showItem(elem.Value)
|
||||||
|
if isNewSearch {
|
||||||
|
start -= len(rs)
|
||||||
|
if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if elem == o.current {
|
||||||
|
if len(item)-1 >= start {
|
||||||
|
item = item[start:]
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx := runes.IndexAllEx(item, rs, o.cfg.HistorySearchFold)
|
||||||
|
if idx < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if elem == o.current {
|
||||||
|
idx += start
|
||||||
|
}
|
||||||
|
return idx, elem
|
||||||
|
}
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) showItem(obj interface{}) []rune {
|
||||||
|
item := obj.(*hisItem)
|
||||||
|
if item.Version == o.historyVer {
|
||||||
|
return item.Tmp
|
||||||
|
}
|
||||||
|
return item.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Prev() []rune {
|
||||||
|
if o.current == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
current := o.current.Prev()
|
||||||
|
if current == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
o.current = current
|
||||||
|
return runes.Copy(o.showItem(current.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Next() ([]rune, bool) {
|
||||||
|
if o.current == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
current := o.current.Next()
|
||||||
|
if current == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
o.current = current
|
||||||
|
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() {
|
||||||
|
Debug("-------")
|
||||||
|
for item := o.history.Front(); item != nil; item = item.Next() {
|
||||||
|
Debug(fmt.Sprintf("%+v", item.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save history
|
||||||
|
func (o *opHistory) New(current []rune) (err error) {
|
||||||
|
|
||||||
|
// history deactivated
|
||||||
|
if !o.enable {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
current = runes.Copy(current)
|
||||||
|
|
||||||
|
// if just use last command without modify
|
||||||
|
// just clean lastest history
|
||||||
|
if back := o.history.Back(); back != nil {
|
||||||
|
prev := back.Prev()
|
||||||
|
if prev != nil {
|
||||||
|
if runes.Equal(current, prev.Value.(*hisItem).Source) {
|
||||||
|
o.current = o.history.Back()
|
||||||
|
o.current.Value.(*hisItem).Clean()
|
||||||
|
o.historyVer++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(current) == 0 {
|
||||||
|
o.current = o.history.Back()
|
||||||
|
if o.current != nil {
|
||||||
|
o.current.Value.(*hisItem).Clean()
|
||||||
|
o.historyVer++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.current != o.history.Back() {
|
||||||
|
// move history item to current command
|
||||||
|
currentItem := o.current.Value.(*hisItem)
|
||||||
|
// set current to last item
|
||||||
|
o.current = o.history.Back()
|
||||||
|
|
||||||
|
current = runes.Copy(currentItem.Tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// err only can be a IO error, just report
|
||||||
|
err = o.Update(current, true)
|
||||||
|
|
||||||
|
// push a new one to commit current command
|
||||||
|
o.historyVer++
|
||||||
|
o.Push(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Revert() {
|
||||||
|
o.historyVer++
|
||||||
|
o.current = o.history.Back()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Update(s []rune, commit bool) (err error) {
|
||||||
|
o.fdLock.Lock()
|
||||||
|
defer o.fdLock.Unlock()
|
||||||
|
s = runes.Copy(s)
|
||||||
|
if o.current == nil {
|
||||||
|
o.Push(s)
|
||||||
|
o.Compact()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := o.current.Value.(*hisItem)
|
||||||
|
r.Version = o.historyVer
|
||||||
|
if commit {
|
||||||
|
r.Source = s
|
||||||
|
if o.fd != nil {
|
||||||
|
// just report the error
|
||||||
|
_, err = o.fd.Write([]byte(string(r.Source) + "\n"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.Tmp = append(r.Tmp[:0], s...)
|
||||||
|
}
|
||||||
|
o.current.Value = r
|
||||||
|
o.Compact()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opHistory) Push(s []rune) {
|
||||||
|
s = runes.Copy(s)
|
||||||
|
elem := o.history.PushBack(&hisItem{Source: s})
|
||||||
|
o.current = elem
|
||||||
|
}
|
|
@ -0,0 +1,537 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInterrupt = errors.New("Interrupt")
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterruptError struct {
|
||||||
|
Line []rune
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*InterruptError) Error() string {
|
||||||
|
return "Interrupted"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Operation struct {
|
||||||
|
m sync.Mutex
|
||||||
|
cfg *Config
|
||||||
|
t *Terminal
|
||||||
|
buf *RuneBuffer
|
||||||
|
outchan chan []rune
|
||||||
|
errchan chan error
|
||||||
|
w io.Writer
|
||||||
|
|
||||||
|
history *opHistory
|
||||||
|
*opSearch
|
||||||
|
*opCompleter
|
||||||
|
*opPassword
|
||||||
|
*opVim
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) SetBuffer(what string) {
|
||||||
|
o.buf.Set([]rune(what))
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrapWriter struct {
|
||||||
|
r *Operation
|
||||||
|
t *Terminal
|
||||||
|
target io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrapWriter) Write(b []byte) (int, error) {
|
||||||
|
if !w.t.IsReading() {
|
||||||
|
return w.target.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
w.r.buf.Refresh(func() {
|
||||||
|
n, err = w.target.Write(b)
|
||||||
|
})
|
||||||
|
|
||||||
|
if w.r.IsSearchMode() {
|
||||||
|
w.r.SearchRefresh(-1)
|
||||||
|
}
|
||||||
|
if w.r.IsInCompleteMode() {
|
||||||
|
w.r.CompleteRefresh()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOperation(t *Terminal, cfg *Config) *Operation {
|
||||||
|
width := cfg.FuncGetWidth()
|
||||||
|
op := &Operation{
|
||||||
|
t: t,
|
||||||
|
buf: NewRuneBuffer(t, cfg.Prompt, cfg, width),
|
||||||
|
outchan: make(chan []rune),
|
||||||
|
errchan: make(chan error, 1),
|
||||||
|
}
|
||||||
|
op.w = op.buf.w
|
||||||
|
op.SetConfig(cfg)
|
||||||
|
op.opVim = newVimMode(op)
|
||||||
|
op.opCompleter = newOpCompleter(op.buf.w, op, width)
|
||||||
|
op.opPassword = newOpPassword(op)
|
||||||
|
op.cfg.FuncOnWidthChanged(func() {
|
||||||
|
newWidth := cfg.FuncGetWidth()
|
||||||
|
op.opCompleter.OnWidthChange(newWidth)
|
||||||
|
op.opSearch.OnWidthChange(newWidth)
|
||||||
|
op.buf.OnWidthChange(newWidth)
|
||||||
|
})
|
||||||
|
go op.ioloop()
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) SetPrompt(s string) {
|
||||||
|
o.buf.SetPrompt(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) SetMaskRune(r rune) {
|
||||||
|
o.buf.SetMask(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) GetConfig() *Config {
|
||||||
|
o.m.Lock()
|
||||||
|
cfg := *o.cfg
|
||||||
|
o.m.Unlock()
|
||||||
|
return &cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) ioloop() {
|
||||||
|
for {
|
||||||
|
keepInSearchMode := false
|
||||||
|
keepInCompleteMode := false
|
||||||
|
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 o.buf.Len() == 0 {
|
||||||
|
o.buf.Clean()
|
||||||
|
select {
|
||||||
|
case o.errchan <- io.EOF:
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
// if stdin got io.EOF and there is something left in buffer,
|
||||||
|
// let's flush them by sending CharEnter.
|
||||||
|
// And we will got io.EOF int next loop.
|
||||||
|
r = CharEnter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isUpdateHistory := true
|
||||||
|
|
||||||
|
if o.IsInCompleteSelectMode() {
|
||||||
|
keepInCompleteMode = o.HandleCompleteSelect(r)
|
||||||
|
if keepInCompleteMode {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
o.buf.Refresh(nil)
|
||||||
|
switch r {
|
||||||
|
case CharEnter, CharCtrlJ:
|
||||||
|
o.history.Update(o.buf.Runes(), false)
|
||||||
|
fallthrough
|
||||||
|
case CharInterrupt:
|
||||||
|
o.t.KickRead()
|
||||||
|
fallthrough
|
||||||
|
case CharBell:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.IsEnableVimMode() {
|
||||||
|
r = o.HandleVim(r, o.t.ReadRune)
|
||||||
|
if r == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r {
|
||||||
|
case CharBell:
|
||||||
|
if o.IsSearchMode() {
|
||||||
|
o.ExitSearchMode(true)
|
||||||
|
o.buf.Refresh(nil)
|
||||||
|
}
|
||||||
|
if o.IsInCompleteMode() {
|
||||||
|
o.ExitCompleteMode(true)
|
||||||
|
o.buf.Refresh(nil)
|
||||||
|
}
|
||||||
|
case CharTab:
|
||||||
|
if o.GetConfig().AutoComplete == nil {
|
||||||
|
o.t.Bell()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if o.OnComplete() {
|
||||||
|
keepInCompleteMode = true
|
||||||
|
} else {
|
||||||
|
o.t.Bell()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case CharBckSearch:
|
||||||
|
if !o.SearchMode(S_DIR_BCK) {
|
||||||
|
o.t.Bell()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
keepInSearchMode = true
|
||||||
|
case CharCtrlU:
|
||||||
|
o.buf.KillFront()
|
||||||
|
case CharFwdSearch:
|
||||||
|
if !o.SearchMode(S_DIR_FWD) {
|
||||||
|
o.t.Bell()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
keepInSearchMode = true
|
||||||
|
case CharKill:
|
||||||
|
o.buf.Kill()
|
||||||
|
keepInCompleteMode = true
|
||||||
|
case MetaForward:
|
||||||
|
o.buf.MoveToNextWord()
|
||||||
|
case CharTranspose:
|
||||||
|
o.buf.Transpose()
|
||||||
|
case MetaBackward:
|
||||||
|
o.buf.MoveToPrevWord()
|
||||||
|
case MetaDelete:
|
||||||
|
o.buf.DeleteWord()
|
||||||
|
case CharLineStart:
|
||||||
|
o.buf.MoveToLineStart()
|
||||||
|
case CharLineEnd:
|
||||||
|
o.buf.MoveToLineEnd()
|
||||||
|
case CharBackspace, CharCtrlH:
|
||||||
|
if o.IsSearchMode() {
|
||||||
|
o.SearchBackspace()
|
||||||
|
keepInSearchMode = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.buf.Len() == 0 {
|
||||||
|
o.t.Bell()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
o.buf.Backspace()
|
||||||
|
if o.IsInCompleteMode() {
|
||||||
|
o.OnComplete()
|
||||||
|
}
|
||||||
|
case CharCtrlZ:
|
||||||
|
o.buf.Clean()
|
||||||
|
o.t.SleepToResume()
|
||||||
|
o.Refresh()
|
||||||
|
case CharCtrlL:
|
||||||
|
ClearScreen(o.w)
|
||||||
|
o.Refresh()
|
||||||
|
case MetaBackspace, CharCtrlW:
|
||||||
|
o.buf.BackEscapeWord()
|
||||||
|
case CharCtrlY:
|
||||||
|
o.buf.Yank()
|
||||||
|
case CharEnter, CharCtrlJ:
|
||||||
|
if o.IsSearchMode() {
|
||||||
|
o.ExitSearchMode(false)
|
||||||
|
}
|
||||||
|
o.buf.MoveToLineEnd()
|
||||||
|
var data []rune
|
||||||
|
if !o.GetConfig().UniqueEditLine {
|
||||||
|
o.buf.WriteRune('\n')
|
||||||
|
data = o.buf.Reset()
|
||||||
|
data = data[:len(data)-1] // trim \n
|
||||||
|
} else {
|
||||||
|
o.buf.Clean()
|
||||||
|
data = o.buf.Reset()
|
||||||
|
}
|
||||||
|
o.outchan <- data
|
||||||
|
if !o.GetConfig().DisableAutoSaveHistory {
|
||||||
|
// ignore IO error
|
||||||
|
_ = o.history.New(data)
|
||||||
|
} else {
|
||||||
|
isUpdateHistory = false
|
||||||
|
}
|
||||||
|
case CharBackward:
|
||||||
|
o.buf.MoveBackward()
|
||||||
|
case CharForward:
|
||||||
|
o.buf.MoveForward()
|
||||||
|
case CharPrev:
|
||||||
|
buf := o.history.Prev()
|
||||||
|
if buf != nil {
|
||||||
|
o.buf.Set(buf)
|
||||||
|
} else {
|
||||||
|
o.t.Bell()
|
||||||
|
}
|
||||||
|
case CharNext:
|
||||||
|
buf, ok := o.history.Next()
|
||||||
|
if ok {
|
||||||
|
o.buf.Set(buf)
|
||||||
|
} else {
|
||||||
|
o.t.Bell()
|
||||||
|
}
|
||||||
|
case CharDelete:
|
||||||
|
if o.buf.Len() > 0 || !o.IsNormalMode() {
|
||||||
|
o.t.KickRead()
|
||||||
|
if !o.buf.Delete() {
|
||||||
|
o.t.Bell()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// treat as EOF
|
||||||
|
if !o.GetConfig().UniqueEditLine {
|
||||||
|
o.buf.WriteString(o.GetConfig().EOFPrompt + "\n")
|
||||||
|
}
|
||||||
|
o.buf.Reset()
|
||||||
|
isUpdateHistory = false
|
||||||
|
o.history.Revert()
|
||||||
|
o.errchan <- io.EOF
|
||||||
|
if o.GetConfig().UniqueEditLine {
|
||||||
|
o.buf.Clean()
|
||||||
|
}
|
||||||
|
case CharInterrupt:
|
||||||
|
if o.IsSearchMode() {
|
||||||
|
o.t.KickRead()
|
||||||
|
o.ExitSearchMode(true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if o.IsInCompleteMode() {
|
||||||
|
o.t.KickRead()
|
||||||
|
o.ExitCompleteMode(true)
|
||||||
|
o.buf.Refresh(nil)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
o.buf.MoveToLineEnd()
|
||||||
|
o.buf.Refresh(nil)
|
||||||
|
hint := o.GetConfig().InterruptPrompt + "\n"
|
||||||
|
if !o.GetConfig().UniqueEditLine {
|
||||||
|
o.buf.WriteString(hint)
|
||||||
|
}
|
||||||
|
remain := o.buf.Reset()
|
||||||
|
if !o.GetConfig().UniqueEditLine {
|
||||||
|
remain = remain[:len(remain)-len([]rune(hint))]
|
||||||
|
}
|
||||||
|
isUpdateHistory = false
|
||||||
|
o.history.Revert()
|
||||||
|
o.errchan <- &InterruptError{remain}
|
||||||
|
default:
|
||||||
|
if o.IsSearchMode() {
|
||||||
|
o.SearchChar(r)
|
||||||
|
keepInSearchMode = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
o.buf.WriteRune(r)
|
||||||
|
if o.IsInCompleteMode() {
|
||||||
|
o.OnComplete()
|
||||||
|
keepInCompleteMode = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listener := o.GetConfig().Listener
|
||||||
|
if listener != nil {
|
||||||
|
newLine, newPos, ok := listener.OnChange(o.buf.Runes(), o.buf.Pos(), r)
|
||||||
|
if ok {
|
||||||
|
o.buf.SetWithIdx(newPos, newLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
o.m.Lock()
|
||||||
|
if !keepInSearchMode && o.IsSearchMode() {
|
||||||
|
o.ExitSearchMode(false)
|
||||||
|
o.buf.Refresh(nil)
|
||||||
|
} else if o.IsInCompleteMode() {
|
||||||
|
if !keepInCompleteMode {
|
||||||
|
o.ExitCompleteMode(false)
|
||||||
|
o.Refresh()
|
||||||
|
} else {
|
||||||
|
o.buf.Refresh(nil)
|
||||||
|
o.CompleteRefresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isUpdateHistory && !o.IsSearchMode() {
|
||||||
|
// it will cause null history
|
||||||
|
o.history.Update(o.buf.Runes(), false)
|
||||||
|
}
|
||||||
|
o.m.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) Stderr() io.Writer {
|
||||||
|
return &wrapWriter{target: o.GetConfig().Stderr, r: o, t: o.t}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) Stdout() io.Writer {
|
||||||
|
return &wrapWriter{target: o.GetConfig().Stdout, r: o, t: o.t}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) String() (string, error) {
|
||||||
|
r, err := o.Runes()
|
||||||
|
return string(r), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) Runes() ([]rune, error) {
|
||||||
|
o.t.EnterRawMode()
|
||||||
|
defer o.t.ExitRawMode()
|
||||||
|
|
||||||
|
listener := o.GetConfig().Listener
|
||||||
|
if listener != nil {
|
||||||
|
listener.OnChange(nil, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
o.buf.Refresh(nil) // print prompt
|
||||||
|
o.t.KickRead()
|
||||||
|
select {
|
||||||
|
case r := <-o.outchan:
|
||||||
|
return r, nil
|
||||||
|
case err := <-o.errchan:
|
||||||
|
if e, ok := err.(*InterruptError); ok {
|
||||||
|
return e.Line, ErrInterrupt
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) PasswordEx(prompt string, l Listener) ([]byte, error) {
|
||||||
|
cfg := o.GenPasswordConfig()
|
||||||
|
cfg.Prompt = prompt
|
||||||
|
cfg.Listener = l
|
||||||
|
return o.PasswordWithConfig(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) GenPasswordConfig() *Config {
|
||||||
|
return o.opPassword.PasswordConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) PasswordWithConfig(cfg *Config) ([]byte, error) {
|
||||||
|
if err := o.opPassword.EnterPasswordMode(cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer o.opPassword.ExitPasswordMode()
|
||||||
|
return o.Slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) Password(prompt string) ([]byte, error) {
|
||||||
|
return o.PasswordEx(prompt, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) SetTitle(t string) {
|
||||||
|
o.w.Write([]byte("\033[2;" + t + "\007"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) Slice() ([]byte, error) {
|
||||||
|
r, err := o.Runes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte(string(r)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) Close() {
|
||||||
|
select {
|
||||||
|
case o.errchan <- io.EOF:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
o.history.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) SetHistoryPath(path string) {
|
||||||
|
if o.history != nil {
|
||||||
|
o.history.Close()
|
||||||
|
}
|
||||||
|
o.cfg.HistoryFile = path
|
||||||
|
o.history = newOpHistory(o.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) IsNormalMode() bool {
|
||||||
|
return !o.IsInCompleteMode() && !o.IsSearchMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
|
||||||
|
op.m.Lock()
|
||||||
|
defer op.m.Unlock()
|
||||||
|
if op.cfg == cfg {
|
||||||
|
return op.cfg, nil
|
||||||
|
}
|
||||||
|
if err := cfg.Init(); err != nil {
|
||||||
|
return op.cfg, err
|
||||||
|
}
|
||||||
|
old := op.cfg
|
||||||
|
op.cfg = cfg
|
||||||
|
op.SetPrompt(cfg.Prompt)
|
||||||
|
op.SetMaskRune(cfg.MaskRune)
|
||||||
|
op.buf.SetConfig(cfg)
|
||||||
|
width := op.cfg.FuncGetWidth()
|
||||||
|
|
||||||
|
if cfg.opHistory == nil {
|
||||||
|
op.SetHistoryPath(cfg.HistoryFile)
|
||||||
|
cfg.opHistory = op.history
|
||||||
|
cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg, width)
|
||||||
|
}
|
||||||
|
op.history = cfg.opHistory
|
||||||
|
|
||||||
|
// SetHistoryPath will close opHistory which already exists
|
||||||
|
// so if we use it next time, we need to reopen it by `InitHistory()`
|
||||||
|
op.history.Init()
|
||||||
|
|
||||||
|
if op.cfg.AutoComplete != nil {
|
||||||
|
op.opCompleter = newOpCompleter(op.buf.w, op, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
op.opSearch = cfg.opSearch
|
||||||
|
return old, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) ResetHistory() {
|
||||||
|
o.history.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// if err is not nil, it just mean it fail to write to file
|
||||||
|
// other things goes fine.
|
||||||
|
func (o *Operation) SaveHistory(content string) error {
|
||||||
|
return o.history.New([]rune(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) Refresh() {
|
||||||
|
if o.t.IsReading() {
|
||||||
|
o.buf.Refresh(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Operation) Clean() {
|
||||||
|
o.buf.Clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuncListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) Listener {
|
||||||
|
return &DumpListener{f: f}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DumpListener struct {
|
||||||
|
f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DumpListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
|
||||||
|
return d.f(line, pos, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Listener interface {
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
type opPassword struct {
|
||||||
|
o *Operation
|
||||||
|
backupCfg *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOpPassword(o *Operation) *opPassword {
|
||||||
|
return &opPassword{o: o}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opPassword) ExitPasswordMode() {
|
||||||
|
o.o.SetConfig(o.backupCfg)
|
||||||
|
o.backupCfg = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) {
|
||||||
|
o.backupCfg, err = o.o.SetConfig(cfg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opPassword) PasswordConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
EnableMask: true,
|
||||||
|
InterruptPrompt: "\n",
|
||||||
|
EOFPrompt: "\n",
|
||||||
|
HistoryLimit: -1,
|
||||||
|
Painter: &defaultPainter{},
|
||||||
|
|
||||||
|
Stdout: o.o.cfg.Stdout,
|
||||||
|
Stderr: o.o.cfg.Stderr,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
const (
|
||||||
|
VK_CANCEL = 0x03
|
||||||
|
VK_BACK = 0x08
|
||||||
|
VK_TAB = 0x09
|
||||||
|
VK_RETURN = 0x0D
|
||||||
|
VK_SHIFT = 0x10
|
||||||
|
VK_CONTROL = 0x11
|
||||||
|
VK_MENU = 0x12
|
||||||
|
VK_ESCAPE = 0x1B
|
||||||
|
VK_LEFT = 0x25
|
||||||
|
VK_UP = 0x26
|
||||||
|
VK_RIGHT = 0x27
|
||||||
|
VK_DOWN = 0x28
|
||||||
|
VK_DELETE = 0x2E
|
||||||
|
VK_LSHIFT = 0xA0
|
||||||
|
VK_RSHIFT = 0xA1
|
||||||
|
VK_LCONTROL = 0xA2
|
||||||
|
VK_RCONTROL = 0xA3
|
||||||
|
)
|
||||||
|
|
||||||
|
// RawReader translate input record to ANSI escape sequence.
|
||||||
|
// To provides same behavior as unix terminal.
|
||||||
|
type RawReader struct {
|
||||||
|
ctrlKey bool
|
||||||
|
altKey bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRawReader() *RawReader {
|
||||||
|
r := new(RawReader)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// only process one action in one read
|
||||||
|
func (r *RawReader) Read(buf []byte) (int, error) {
|
||||||
|
ir := new(_INPUT_RECORD)
|
||||||
|
var read int
|
||||||
|
var err error
|
||||||
|
next:
|
||||||
|
err = kernel.ReadConsoleInputW(stdin,
|
||||||
|
uintptr(unsafe.Pointer(ir)),
|
||||||
|
1,
|
||||||
|
uintptr(unsafe.Pointer(&read)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if ir.EventType != EVENT_KEY {
|
||||||
|
goto next
|
||||||
|
}
|
||||||
|
ker := (*_KEY_EVENT_RECORD)(unsafe.Pointer(&ir.Event[0]))
|
||||||
|
if ker.bKeyDown == 0 { // keyup
|
||||||
|
if r.ctrlKey || r.altKey {
|
||||||
|
switch ker.wVirtualKeyCode {
|
||||||
|
case VK_RCONTROL, VK_LCONTROL:
|
||||||
|
r.ctrlKey = false
|
||||||
|
case VK_MENU: //alt
|
||||||
|
r.altKey = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
goto next
|
||||||
|
}
|
||||||
|
|
||||||
|
if ker.unicodeChar == 0 {
|
||||||
|
var target rune
|
||||||
|
switch ker.wVirtualKeyCode {
|
||||||
|
case VK_RCONTROL, VK_LCONTROL:
|
||||||
|
r.ctrlKey = true
|
||||||
|
case VK_MENU: //alt
|
||||||
|
r.altKey = true
|
||||||
|
case VK_LEFT:
|
||||||
|
target = CharBackward
|
||||||
|
case VK_RIGHT:
|
||||||
|
target = CharForward
|
||||||
|
case VK_UP:
|
||||||
|
target = CharPrev
|
||||||
|
case VK_DOWN:
|
||||||
|
target = CharNext
|
||||||
|
}
|
||||||
|
if target != 0 {
|
||||||
|
return r.write(buf, target)
|
||||||
|
}
|
||||||
|
goto next
|
||||||
|
}
|
||||||
|
char := rune(ker.unicodeChar)
|
||||||
|
if r.ctrlKey {
|
||||||
|
switch char {
|
||||||
|
case 'A':
|
||||||
|
char = CharLineStart
|
||||||
|
case 'E':
|
||||||
|
char = CharLineEnd
|
||||||
|
case 'R':
|
||||||
|
char = CharBckSearch
|
||||||
|
case 'S':
|
||||||
|
char = CharFwdSearch
|
||||||
|
}
|
||||||
|
} else if r.altKey {
|
||||||
|
switch char {
|
||||||
|
case VK_BACK:
|
||||||
|
char = CharBackspace
|
||||||
|
}
|
||||||
|
return r.writeEsc(buf, char)
|
||||||
|
}
|
||||||
|
return r.write(buf, char)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawReader) writeEsc(b []byte, char rune) (int, error) {
|
||||||
|
b[0] = '\033'
|
||||||
|
n := copy(b[1:], []byte(string(char)))
|
||||||
|
return n + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawReader) write(b []byte, char rune) (int, error) {
|
||||||
|
n := copy(b, []byte(string(char)))
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawReader) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,338 @@
|
||||||
|
// Readline is a pure go implementation for GNU-Readline kind library.
|
||||||
|
//
|
||||||
|
// example:
|
||||||
|
// rl, err := readline.New("> ")
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer rl.Close()
|
||||||
|
//
|
||||||
|
// for {
|
||||||
|
// line, err := rl.Readline()
|
||||||
|
// if err != nil { // io.EOF
|
||||||
|
// break
|
||||||
|
// }
|
||||||
|
// println(line)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Instance struct {
|
||||||
|
Config *Config
|
||||||
|
Terminal *Terminal
|
||||||
|
Operation *Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// prompt supports ANSI escape sequence, so we can color some characters even in windows
|
||||||
|
Prompt string
|
||||||
|
|
||||||
|
// readline will persist historys to file where HistoryFile specified
|
||||||
|
HistoryFile string
|
||||||
|
// specify the max length of historys, it's 500 by default, set it to -1 to disable history
|
||||||
|
HistoryLimit int
|
||||||
|
DisableAutoSaveHistory bool
|
||||||
|
// enable case-insensitive history searching
|
||||||
|
HistorySearchFold bool
|
||||||
|
|
||||||
|
// AutoCompleter will called once user press TAB
|
||||||
|
AutoComplete AutoCompleter
|
||||||
|
|
||||||
|
// Any key press will pass to Listener
|
||||||
|
// NOTE: Listener will be triggered by (nil, 0, 0) immediately
|
||||||
|
Listener Listener
|
||||||
|
|
||||||
|
Painter Painter
|
||||||
|
|
||||||
|
// If VimMode is true, readline will in vim.insert mode by default
|
||||||
|
VimMode bool
|
||||||
|
|
||||||
|
InterruptPrompt string
|
||||||
|
EOFPrompt string
|
||||||
|
|
||||||
|
FuncGetWidth func() int
|
||||||
|
|
||||||
|
Stdin io.ReadCloser
|
||||||
|
StdinWriter io.Writer
|
||||||
|
Stdout io.Writer
|
||||||
|
Stderr io.Writer
|
||||||
|
|
||||||
|
EnableMask bool
|
||||||
|
MaskRune rune
|
||||||
|
|
||||||
|
// erase the editing line after user submited it
|
||||||
|
// it use in IM usually.
|
||||||
|
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
|
||||||
|
FuncIsTerminal func() bool
|
||||||
|
FuncMakeRaw func() error
|
||||||
|
FuncExitRaw func() error
|
||||||
|
FuncOnWidthChanged func(func())
|
||||||
|
ForceUseInteractive bool
|
||||||
|
|
||||||
|
// private fields
|
||||||
|
inited bool
|
||||||
|
opHistory *opHistory
|
||||||
|
opSearch *opSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) useInteractive() bool {
|
||||||
|
if c.ForceUseInteractive {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return c.FuncIsTerminal()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Init() error {
|
||||||
|
if c.inited {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.inited = true
|
||||||
|
if c.Stdin == nil {
|
||||||
|
c.Stdin = NewCancelableStdin(Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Stdin, c.StdinWriter = NewFillableStdin(c.Stdin)
|
||||||
|
|
||||||
|
if c.Stdout == nil {
|
||||||
|
c.Stdout = Stdout
|
||||||
|
}
|
||||||
|
if c.Stderr == nil {
|
||||||
|
c.Stderr = Stderr
|
||||||
|
}
|
||||||
|
if c.HistoryLimit == 0 {
|
||||||
|
c.HistoryLimit = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InterruptPrompt == "" {
|
||||||
|
c.InterruptPrompt = "^C"
|
||||||
|
} else if c.InterruptPrompt == "\n" {
|
||||||
|
c.InterruptPrompt = ""
|
||||||
|
}
|
||||||
|
if c.EOFPrompt == "" {
|
||||||
|
c.EOFPrompt = "^D"
|
||||||
|
} else if c.EOFPrompt == "\n" {
|
||||||
|
c.EOFPrompt = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AutoComplete == nil {
|
||||||
|
c.AutoComplete = &TabCompleter{}
|
||||||
|
}
|
||||||
|
if c.FuncGetWidth == nil {
|
||||||
|
c.FuncGetWidth = GetScreenWidth
|
||||||
|
}
|
||||||
|
if c.FuncIsTerminal == nil {
|
||||||
|
c.FuncIsTerminal = DefaultIsTerminal
|
||||||
|
}
|
||||||
|
rm := new(RawMode)
|
||||||
|
if c.FuncMakeRaw == nil {
|
||||||
|
c.FuncMakeRaw = rm.Enter
|
||||||
|
}
|
||||||
|
if c.FuncExitRaw == nil {
|
||||||
|
c.FuncExitRaw = rm.Exit
|
||||||
|
}
|
||||||
|
if c.FuncOnWidthChanged == nil {
|
||||||
|
c.FuncOnWidthChanged = DefaultOnWidthChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Config) Clone() *Config {
|
||||||
|
c.opHistory = nil
|
||||||
|
c.opSearch = nil
|
||||||
|
return &c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) {
|
||||||
|
c.Listener = FuncListener(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetPainter(p Painter) {
|
||||||
|
c.Painter = p
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEx(cfg *Config) (*Instance, error) {
|
||||||
|
t, err := NewTerminal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rl := t.Readline()
|
||||||
|
if cfg.Painter == nil {
|
||||||
|
cfg.Painter = &defaultPainter{}
|
||||||
|
}
|
||||||
|
return &Instance{
|
||||||
|
Config: cfg,
|
||||||
|
Terminal: t,
|
||||||
|
Operation: rl,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(prompt string) (*Instance, error) {
|
||||||
|
return NewEx(&Config{Prompt: prompt})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) ResetHistory() {
|
||||||
|
i.Operation.ResetHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) SetPrompt(s string) {
|
||||||
|
i.Operation.SetPrompt(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) SetMaskRune(r rune) {
|
||||||
|
i.Operation.SetMaskRune(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// change history persistence in runtime
|
||||||
|
func (i *Instance) SetHistoryPath(p string) {
|
||||||
|
i.Operation.SetHistoryPath(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readline will refresh automatic when write through Stdout()
|
||||||
|
func (i *Instance) Stdout() io.Writer {
|
||||||
|
return i.Operation.Stdout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// readline will refresh automatic when write through Stdout()
|
||||||
|
func (i *Instance) Stderr() io.Writer {
|
||||||
|
return i.Operation.Stderr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch VimMode in runtime
|
||||||
|
func (i *Instance) SetVimMode(on bool) {
|
||||||
|
i.Operation.SetVimMode(on)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) IsVimMode() bool {
|
||||||
|
return i.Operation.IsEnableVimMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) GenPasswordConfig() *Config {
|
||||||
|
return i.Operation.GenPasswordConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can generate a config by `i.GenPasswordConfig()`
|
||||||
|
func (i *Instance) ReadPasswordWithConfig(cfg *Config) ([]byte, error) {
|
||||||
|
return i.Operation.PasswordWithConfig(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) ReadPasswordEx(prompt string, l Listener) ([]byte, error) {
|
||||||
|
return i.Operation.PasswordEx(prompt, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) ReadPassword(prompt string) ([]byte, error) {
|
||||||
|
return i.Operation.Password(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Line string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Result) CanContinue() bool {
|
||||||
|
return len(l.Line) != 0 && l.Error == ErrInterrupt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Result) CanBreak() bool {
|
||||||
|
return !l.CanContinue() && l.Error != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) Line() *Result {
|
||||||
|
ret, err := i.Readline()
|
||||||
|
return &Result{ret, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// err is one of (nil, io.EOF, readline.ErrInterrupt)
|
||||||
|
func (i *Instance) Readline() (string, error) {
|
||||||
|
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 {
|
||||||
|
return i.Operation.SaveHistory(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// same as readline
|
||||||
|
func (i *Instance) ReadSlice() ([]byte, error) {
|
||||||
|
return i.Operation.Slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
i.Config.Stdin.Close()
|
||||||
|
i.Operation.Close()
|
||||||
|
if err := i.Terminal.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// call CaptureExitSignal when you want readline exit gracefully.
|
||||||
|
func (i *Instance) CaptureExitSignal() {
|
||||||
|
CaptureExitSignal(func() {
|
||||||
|
i.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) Clean() {
|
||||||
|
i.Operation.Clean()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) Write(b []byte) (int, error) {
|
||||||
|
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 {
|
||||||
|
if i.Config == cfg {
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
old := i.Config
|
||||||
|
i.Config = cfg
|
||||||
|
i.Operation.SetConfig(cfg)
|
||||||
|
i.Terminal.SetConfig(cfg)
|
||||||
|
return old
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Instance) 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()
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRace(t *testing.T) {
|
||||||
|
rl, err := NewEx(&Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for range time.Tick(time.Millisecond) {
|
||||||
|
rl.SetPrompt("hello")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
rl.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
rl.Readline()
|
||||||
|
}
|
|
@ -0,0 +1,475 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MsgType int16
|
||||||
|
|
||||||
|
const (
|
||||||
|
T_DATA = MsgType(iota)
|
||||||
|
T_WIDTH
|
||||||
|
T_WIDTH_REPORT
|
||||||
|
T_ISTTY_REPORT
|
||||||
|
T_RAW
|
||||||
|
T_ERAW // exit raw
|
||||||
|
T_EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
type RemoteSvr struct {
|
||||||
|
eof int32
|
||||||
|
closed int32
|
||||||
|
width int32
|
||||||
|
reciveChan chan struct{}
|
||||||
|
writeChan chan *writeCtx
|
||||||
|
conn net.Conn
|
||||||
|
isTerminal bool
|
||||||
|
funcWidthChan func()
|
||||||
|
stopChan chan struct{}
|
||||||
|
|
||||||
|
dataBufM sync.Mutex
|
||||||
|
dataBuf bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeReply struct {
|
||||||
|
n int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeCtx struct {
|
||||||
|
msg *Message
|
||||||
|
reply chan *writeReply
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWriteCtx(msg *Message) *writeCtx {
|
||||||
|
return &writeCtx{
|
||||||
|
msg: msg,
|
||||||
|
reply: make(chan *writeReply),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoteSvr(conn net.Conn) (*RemoteSvr, error) {
|
||||||
|
rs := &RemoteSvr{
|
||||||
|
width: -1,
|
||||||
|
conn: conn,
|
||||||
|
writeChan: make(chan *writeCtx),
|
||||||
|
reciveChan: make(chan struct{}),
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
buf := bufio.NewReader(rs.conn)
|
||||||
|
|
||||||
|
if err := rs.init(buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go rs.readLoop(buf)
|
||||||
|
go rs.writeLoop()
|
||||||
|
return rs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) init(buf *bufio.Reader) error {
|
||||||
|
m, err := ReadMessage(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// receive isTerminal
|
||||||
|
if m.Type != T_ISTTY_REPORT {
|
||||||
|
return fmt.Errorf("unexpected init message")
|
||||||
|
}
|
||||||
|
r.GotIsTerminal(m.Data)
|
||||||
|
|
||||||
|
// receive width
|
||||||
|
m, err = ReadMessage(buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if m.Type != T_WIDTH_REPORT {
|
||||||
|
return fmt.Errorf("unexpected init message")
|
||||||
|
}
|
||||||
|
r.GotReportWidth(m.Data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) HandleConfig(cfg *Config) {
|
||||||
|
cfg.Stderr = r
|
||||||
|
cfg.Stdout = r
|
||||||
|
cfg.Stdin = r
|
||||||
|
cfg.FuncExitRaw = r.ExitRawMode
|
||||||
|
cfg.FuncIsTerminal = r.IsTerminal
|
||||||
|
cfg.FuncMakeRaw = r.EnterRawMode
|
||||||
|
cfg.FuncExitRaw = r.ExitRawMode
|
||||||
|
cfg.FuncGetWidth = r.GetWidth
|
||||||
|
cfg.FuncOnWidthChanged = func(f func()) {
|
||||||
|
r.funcWidthChan = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) IsTerminal() bool {
|
||||||
|
return r.isTerminal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) checkEOF() error {
|
||||||
|
if atomic.LoadInt32(&r.eof) == 1 {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) Read(b []byte) (int, error) {
|
||||||
|
r.dataBufM.Lock()
|
||||||
|
n, err := r.dataBuf.Read(b)
|
||||||
|
r.dataBufM.Unlock()
|
||||||
|
if n == 0 {
|
||||||
|
if err := r.checkEOF(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == 0 && err == io.EOF {
|
||||||
|
<-r.reciveChan
|
||||||
|
r.dataBufM.Lock()
|
||||||
|
n, err = r.dataBuf.Read(b)
|
||||||
|
r.dataBufM.Unlock()
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
if err := r.checkEOF(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) writeMsg(m *Message) error {
|
||||||
|
ctx := newWriteCtx(m)
|
||||||
|
r.writeChan <- ctx
|
||||||
|
reply := <-ctx.reply
|
||||||
|
return reply.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) Write(b []byte) (int, error) {
|
||||||
|
ctx := newWriteCtx(NewMessage(T_DATA, b))
|
||||||
|
r.writeChan <- ctx
|
||||||
|
reply := <-ctx.reply
|
||||||
|
return reply.n, reply.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) EnterRawMode() error {
|
||||||
|
return r.writeMsg(NewMessage(T_RAW, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) ExitRawMode() error {
|
||||||
|
return r.writeMsg(NewMessage(T_ERAW, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) writeLoop() {
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case ctx, ok := <-r.writeChan:
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n, err := ctx.msg.WriteTo(r.conn)
|
||||||
|
ctx.reply <- &writeReply{n, err}
|
||||||
|
case <-r.stopChan:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) Close() error {
|
||||||
|
if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
|
||||||
|
close(r.stopChan)
|
||||||
|
r.conn.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) readLoop(buf *bufio.Reader) {
|
||||||
|
defer r.Close()
|
||||||
|
for {
|
||||||
|
m, err := ReadMessage(buf)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch m.Type {
|
||||||
|
case T_EOF:
|
||||||
|
atomic.StoreInt32(&r.eof, 1)
|
||||||
|
select {
|
||||||
|
case r.reciveChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
case T_DATA:
|
||||||
|
r.dataBufM.Lock()
|
||||||
|
r.dataBuf.Write(m.Data)
|
||||||
|
r.dataBufM.Unlock()
|
||||||
|
select {
|
||||||
|
case r.reciveChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
case T_WIDTH_REPORT:
|
||||||
|
r.GotReportWidth(m.Data)
|
||||||
|
case T_ISTTY_REPORT:
|
||||||
|
r.GotIsTerminal(m.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) GotIsTerminal(data []byte) {
|
||||||
|
if binary.BigEndian.Uint16(data) == 0 {
|
||||||
|
r.isTerminal = false
|
||||||
|
} else {
|
||||||
|
r.isTerminal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) GotReportWidth(data []byte) {
|
||||||
|
atomic.StoreInt32(&r.width, int32(binary.BigEndian.Uint16(data)))
|
||||||
|
if r.funcWidthChan != nil {
|
||||||
|
r.funcWidthChan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteSvr) GetWidth() int {
|
||||||
|
return int(atomic.LoadInt32(&r.width))
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Type MsgType
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadMessage(r io.Reader) (*Message, error) {
|
||||||
|
m := new(Message)
|
||||||
|
var length int32
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := binary.Read(r, binary.BigEndian, &m.Type); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m.Data = make([]byte, int(length)-2)
|
||||||
|
if _, err := io.ReadFull(r, m.Data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessage(t MsgType, data []byte) *Message {
|
||||||
|
return &Message{t, data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) WriteTo(w io.Writer) (int, error) {
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, len(m.Data)+2+4))
|
||||||
|
binary.Write(buf, binary.BigEndian, int32(len(m.Data)+2))
|
||||||
|
binary.Write(buf, binary.BigEndian, m.Type)
|
||||||
|
buf.Write(m.Data)
|
||||||
|
n, err := buf.WriteTo(w)
|
||||||
|
return int(n), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type RemoteCli struct {
|
||||||
|
conn net.Conn
|
||||||
|
raw RawMode
|
||||||
|
receiveChan chan struct{}
|
||||||
|
inited int32
|
||||||
|
isTerminal *bool
|
||||||
|
|
||||||
|
data bytes.Buffer
|
||||||
|
dataM sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoteCli(conn net.Conn) (*RemoteCli, error) {
|
||||||
|
r := &RemoteCli{
|
||||||
|
conn: conn,
|
||||||
|
receiveChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) MarkIsTerminal(is bool) {
|
||||||
|
r.isTerminal = &is
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) init() error {
|
||||||
|
if !atomic.CompareAndSwapInt32(&r.inited, 0, 1) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reportIsTerminal(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reportWidth(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// register sig for width changed
|
||||||
|
DefaultOnWidthChanged(func() {
|
||||||
|
r.reportWidth()
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) writeMsg(m *Message) error {
|
||||||
|
r.dataM.Lock()
|
||||||
|
_, err := m.WriteTo(r.conn)
|
||||||
|
r.dataM.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) Write(b []byte) (int, error) {
|
||||||
|
m := NewMessage(T_DATA, b)
|
||||||
|
r.dataM.Lock()
|
||||||
|
_, err := m.WriteTo(r.conn)
|
||||||
|
r.dataM.Unlock()
|
||||||
|
return len(b), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) reportWidth() error {
|
||||||
|
screenWidth := GetScreenWidth()
|
||||||
|
data := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(data, uint16(screenWidth))
|
||||||
|
msg := NewMessage(T_WIDTH_REPORT, data)
|
||||||
|
|
||||||
|
if err := r.writeMsg(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) reportIsTerminal() error {
|
||||||
|
var isTerminal bool
|
||||||
|
if r.isTerminal != nil {
|
||||||
|
isTerminal = *r.isTerminal
|
||||||
|
} else {
|
||||||
|
isTerminal = DefaultIsTerminal()
|
||||||
|
}
|
||||||
|
data := make([]byte, 2)
|
||||||
|
if isTerminal {
|
||||||
|
binary.BigEndian.PutUint16(data, 1)
|
||||||
|
} else {
|
||||||
|
binary.BigEndian.PutUint16(data, 0)
|
||||||
|
}
|
||||||
|
msg := NewMessage(T_ISTTY_REPORT, data)
|
||||||
|
if err := r.writeMsg(msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) readLoop() {
|
||||||
|
buf := bufio.NewReader(r.conn)
|
||||||
|
for {
|
||||||
|
msg, err := ReadMessage(buf)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch msg.Type {
|
||||||
|
case T_ERAW:
|
||||||
|
r.raw.Exit()
|
||||||
|
case T_RAW:
|
||||||
|
r.raw.Enter()
|
||||||
|
case T_DATA:
|
||||||
|
os.Stdout.Write(msg.Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) ServeBy(source io.Reader) error {
|
||||||
|
if err := r.init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer r.Close()
|
||||||
|
for {
|
||||||
|
n, _ := io.Copy(r, source)
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer r.raw.Exit()
|
||||||
|
r.readLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) Close() {
|
||||||
|
r.writeMsg(NewMessage(T_EOF, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RemoteCli) Serve() error {
|
||||||
|
return r.ServeBy(os.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListenRemote(n, addr string, cfg *Config, h func(*Instance), onListen ...func(net.Listener) error) error {
|
||||||
|
ln, err := net.Listen(n, addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(onListen) > 0 {
|
||||||
|
if err := onListen[0](ln); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer conn.Close()
|
||||||
|
rl, err := HandleConn(*cfg, conn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h(rl)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func HandleConn(cfg Config, conn net.Conn) (*Instance, error) {
|
||||||
|
r, err := NewRemoteSvr(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.HandleConfig(&cfg)
|
||||||
|
|
||||||
|
rl, err := NewEx(&cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DialRemote(n, addr string) error {
|
||||||
|
conn, err := net.Dial(n, addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
cli, err := NewRemoteCli(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cli.Serve()
|
||||||
|
}
|
|
@ -0,0 +1,629 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type runeBufferBck struct {
|
||||||
|
buf []rune
|
||||||
|
idx int
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuneBuffer struct {
|
||||||
|
buf []rune
|
||||||
|
idx int
|
||||||
|
prompt []rune
|
||||||
|
w io.Writer
|
||||||
|
|
||||||
|
hadClean bool
|
||||||
|
interactive bool
|
||||||
|
cfg *Config
|
||||||
|
|
||||||
|
width int
|
||||||
|
|
||||||
|
bck *runeBufferBck
|
||||||
|
|
||||||
|
offset string
|
||||||
|
|
||||||
|
lastKill []rune
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) pushKill(text []rune) {
|
||||||
|
r.lastKill = append([]rune{}, text...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) OnWidthChange(newWidth int) {
|
||||||
|
r.Lock()
|
||||||
|
r.width = newWidth
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Backup() {
|
||||||
|
r.Lock()
|
||||||
|
r.bck = &runeBufferBck{r.buf, r.idx}
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Restore() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.bck == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.buf = r.bck.buf
|
||||||
|
r.idx = r.bck.idx
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRuneBuffer(w io.Writer, prompt string, cfg *Config, width int) *RuneBuffer {
|
||||||
|
rb := &RuneBuffer{
|
||||||
|
w: w,
|
||||||
|
interactive: cfg.useInteractive(),
|
||||||
|
cfg: cfg,
|
||||||
|
width: width,
|
||||||
|
}
|
||||||
|
rb.SetPrompt(prompt)
|
||||||
|
return rb
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) SetConfig(cfg *Config) {
|
||||||
|
r.Lock()
|
||||||
|
r.cfg = cfg
|
||||||
|
r.interactive = cfg.useInteractive()
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) SetMask(m rune) {
|
||||||
|
r.Lock()
|
||||||
|
r.cfg.MaskRune = m
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) CurrentWidth(x int) int {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
return runes.WidthAll(r.buf[:x])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) PromptLen() int {
|
||||||
|
r.Lock()
|
||||||
|
width := r.promptLen()
|
||||||
|
r.Unlock()
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) promptLen() int {
|
||||||
|
return runes.WidthAll(runes.ColorFilter(r.prompt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) RuneSlice(i int) []rune {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
|
||||||
|
if i > 0 {
|
||||||
|
rs := make([]rune, i)
|
||||||
|
copy(rs, r.buf[r.idx:r.idx+i])
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
rs := make([]rune, -i)
|
||||||
|
copy(rs, r.buf[r.idx+i:r.idx])
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Runes() []rune {
|
||||||
|
r.Lock()
|
||||||
|
newr := make([]rune, len(r.buf))
|
||||||
|
copy(newr, r.buf)
|
||||||
|
r.Unlock()
|
||||||
|
return newr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Pos() int {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
return r.idx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Len() int {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
return len(r.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveToLineStart() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.idx = 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveBackward() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.idx--
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) WriteString(s string) {
|
||||||
|
r.WriteRunes([]rune(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) WriteRune(s rune) {
|
||||||
|
r.WriteRunes([]rune{s})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) WriteRunes(s []rune) {
|
||||||
|
r.Refresh(func() {
|
||||||
|
tail := append(s, r.buf[r.idx:]...)
|
||||||
|
r.buf = append(r.buf[:r.idx], tail...)
|
||||||
|
r.idx += len(s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveForward() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == len(r.buf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.idx++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) IsCursorInEnd() bool {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
return r.idx == len(r.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Replace(ch rune) {
|
||||||
|
r.Refresh(func() {
|
||||||
|
r.buf[r.idx] = ch
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Erase() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
r.idx = 0
|
||||||
|
r.pushKill(r.buf[:])
|
||||||
|
r.buf = r.buf[:0]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Delete() (success bool) {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == len(r.buf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.pushKill(r.buf[r.idx : r.idx+1])
|
||||||
|
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
|
||||||
|
success = true
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) DeleteWord() {
|
||||||
|
if r.idx == len(r.buf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
init := r.idx
|
||||||
|
for init < len(r.buf) && IsWordBreak(r.buf[init]) {
|
||||||
|
init++
|
||||||
|
}
|
||||||
|
for i := init + 1; i < len(r.buf); i++ {
|
||||||
|
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
||||||
|
r.pushKill(r.buf[r.idx : i-1])
|
||||||
|
r.Refresh(func() {
|
||||||
|
r.buf = append(r.buf[:r.idx], r.buf[i-1:]...)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveToPrevWord() (success bool) {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := r.idx - 1; i > 0; i-- {
|
||||||
|
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
||||||
|
r.idx = i
|
||||||
|
success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.idx = 0
|
||||||
|
success = true
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) KillFront() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
length := len(r.buf) - r.idx
|
||||||
|
r.pushKill(r.buf[:r.idx])
|
||||||
|
copy(r.buf[:length], r.buf[r.idx:])
|
||||||
|
r.idx = 0
|
||||||
|
r.buf = r.buf[:length]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Kill() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
r.pushKill(r.buf[r.idx:])
|
||||||
|
r.buf = r.buf[:r.idx]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Transpose() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if len(r.buf) == 1 {
|
||||||
|
r.idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.buf) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.idx == 0 {
|
||||||
|
r.idx = 1
|
||||||
|
} else if r.idx >= len(r.buf) {
|
||||||
|
r.idx = len(r.buf) - 1
|
||||||
|
}
|
||||||
|
r.buf[r.idx], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx]
|
||||||
|
r.idx++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveToNextWord() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
for i := r.idx + 1; i < len(r.buf); i++ {
|
||||||
|
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
||||||
|
r.idx = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.idx = len(r.buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveToEndWord() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
// already at the end, so do nothing
|
||||||
|
if r.idx == len(r.buf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if we are at the end of a word already, go to next
|
||||||
|
if !IsWordBreak(r.buf[r.idx]) && IsWordBreak(r.buf[r.idx+1]) {
|
||||||
|
r.idx++
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep going until at the end of a word
|
||||||
|
for i := r.idx + 1; i < len(r.buf); i++ {
|
||||||
|
if IsWordBreak(r.buf[i]) && !IsWordBreak(r.buf[i-1]) {
|
||||||
|
r.idx = i - 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.idx = len(r.buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) BackEscapeWord() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := r.idx - 1; i > 0; i-- {
|
||||||
|
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.idx = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.buf = r.buf[:0]
|
||||||
|
r.idx = 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.idx--
|
||||||
|
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveToLineEnd() {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if r.idx == len(r.buf) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.idx = len(r.buf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) LineCount(width int) int {
|
||||||
|
if width == -1 {
|
||||||
|
width = r.width
|
||||||
|
}
|
||||||
|
return LineCount(width,
|
||||||
|
runes.WidthAll(r.buf)+r.PromptLen())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) {
|
||||||
|
r.Refresh(func() {
|
||||||
|
if reverse {
|
||||||
|
for i := r.idx - 1; i >= 0; i-- {
|
||||||
|
if r.buf[i] == ch {
|
||||||
|
r.idx = i
|
||||||
|
if prevChar {
|
||||||
|
r.idx++
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := r.idx + 1; i < len(r.buf); i++ {
|
||||||
|
if r.buf[i] == ch {
|
||||||
|
r.idx = i
|
||||||
|
if prevChar {
|
||||||
|
r.idx--
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) isInLineEdge() bool {
|
||||||
|
if isWindows {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sp := r.getSplitByLine(r.buf)
|
||||||
|
return len(sp[len(sp)-1]) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) getSplitByLine(rs []rune) []string {
|
||||||
|
return SplitByLine(r.promptLen(), r.width, rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) IdxLine(width int) int {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
return r.idxLine(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) idxLine(width int) int {
|
||||||
|
if width == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
sp := r.getSplitByLine(r.buf[:r.idx])
|
||||||
|
return len(sp) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) CursorLineCount() int {
|
||||||
|
return r.LineCount(r.width) - r.IdxLine(r.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Refresh(f func()) {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
|
||||||
|
if !r.interactive {
|
||||||
|
if f != nil {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.clean()
|
||||||
|
if f != nil {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
r.print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) SetOffset(offset string) {
|
||||||
|
r.Lock()
|
||||||
|
r.offset = offset
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) print() {
|
||||||
|
r.w.Write(r.output())
|
||||||
|
r.hadClean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) output() []byte {
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
buf.WriteString(string(r.prompt))
|
||||||
|
if r.cfg.EnableMask && len(r.buf) > 0 {
|
||||||
|
buf.Write([]byte(strings.Repeat(string(r.cfg.MaskRune), len(r.buf)-1)))
|
||||||
|
if r.buf[len(r.buf)-1] == '\n' {
|
||||||
|
buf.Write([]byte{'\n'})
|
||||||
|
} else {
|
||||||
|
buf.Write([]byte(string(r.cfg.MaskRune)))
|
||||||
|
}
|
||||||
|
if len(r.buf) > r.idx {
|
||||||
|
buf.Write(r.getBackspaceSequence())
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
for _, e := range r.cfg.Painter.Paint(r.buf, r.idx) {
|
||||||
|
if e == '\t' {
|
||||||
|
buf.WriteString(strings.Repeat(" ", TabWidth))
|
||||||
|
} else {
|
||||||
|
buf.WriteRune(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r.isInLineEdge() {
|
||||||
|
buf.Write([]byte(" \b"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cursor position
|
||||||
|
if len(r.buf) > r.idx {
|
||||||
|
buf.Write(r.getBackspaceSequence())
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
ret := runes.Copy(r.buf)
|
||||||
|
r.buf = r.buf[:0]
|
||||||
|
r.idx = 0
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) calWidth(m int) int {
|
||||||
|
if m > 0 {
|
||||||
|
return runes.WidthAll(r.buf[r.idx : r.idx+m])
|
||||||
|
}
|
||||||
|
return runes.WidthAll(r.buf[r.idx+m : r.idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) SetStyle(start, end int, style string) {
|
||||||
|
if end < start {
|
||||||
|
panic("end < start")
|
||||||
|
}
|
||||||
|
|
||||||
|
// goto start
|
||||||
|
move := start - r.idx
|
||||||
|
if move > 0 {
|
||||||
|
r.w.Write([]byte(string(r.buf[r.idx : r.idx+move])))
|
||||||
|
} else {
|
||||||
|
r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move)))
|
||||||
|
}
|
||||||
|
r.w.Write([]byte("\033[" + style + "m"))
|
||||||
|
r.w.Write([]byte(string(r.buf[start:end])))
|
||||||
|
r.w.Write([]byte("\033[0m"))
|
||||||
|
// TODO: move back
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) {
|
||||||
|
r.Refresh(func() {
|
||||||
|
r.buf = buf
|
||||||
|
r.idx = idx
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Set(buf []rune) {
|
||||||
|
r.SetWithIdx(len(buf), buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) SetPrompt(prompt string) {
|
||||||
|
r.Lock()
|
||||||
|
r.prompt = []rune(prompt)
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) {
|
||||||
|
buf := bufio.NewWriter(w)
|
||||||
|
|
||||||
|
if r.width == 0 {
|
||||||
|
buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen()))
|
||||||
|
buf.Write([]byte("\033[J"))
|
||||||
|
} else {
|
||||||
|
buf.Write([]byte("\033[J")) // just like ^k :)
|
||||||
|
if idxLine == 0 {
|
||||||
|
buf.WriteString("\033[2K")
|
||||||
|
buf.WriteString("\r")
|
||||||
|
} else {
|
||||||
|
for i := 0; i < idxLine; i++ {
|
||||||
|
io.WriteString(buf, "\033[2K\r\033[A")
|
||||||
|
}
|
||||||
|
io.WriteString(buf, "\033[2K\r")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.Flush()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) Clean() {
|
||||||
|
r.Lock()
|
||||||
|
r.clean()
|
||||||
|
r.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) clean() {
|
||||||
|
r.cleanWithIdxLine(r.idxLine(r.width))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RuneBuffer) cleanWithIdxLine(idxLine int) {
|
||||||
|
if r.hadClean || !r.interactive {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.hadClean = true
|
||||||
|
r.cleanOutput(r.w, idxLine)
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var runes = Runes{}
|
||||||
|
var TabWidth = 4
|
||||||
|
|
||||||
|
type Runes struct{}
|
||||||
|
|
||||||
|
func (Runes) EqualRune(a, b rune, fold bool) bool {
|
||||||
|
if a == b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !fold {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if a > b {
|
||||||
|
a, b = b, a
|
||||||
|
}
|
||||||
|
if b < utf8.RuneSelf && 'A' <= a && a <= 'Z' {
|
||||||
|
if b == a+'a'-'A' {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Runes) EqualRuneFold(a, b rune) bool {
|
||||||
|
return r.EqualRune(a, b, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Runes) EqualFold(a, b []rune) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
if r.EqualRuneFold(a[i], b[i]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) Equal(a, b []rune) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs Runes) IndexAllBckEx(r, sub []rune, fold bool) int {
|
||||||
|
for i := len(r) - len(sub); i >= 0; i-- {
|
||||||
|
found := true
|
||||||
|
for j := 0; j < len(sub); j++ {
|
||||||
|
if !rs.EqualRune(r[i+j], sub[j], fold) {
|
||||||
|
found = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in runes from end to front
|
||||||
|
func (rs Runes) IndexAllBck(r, sub []rune) int {
|
||||||
|
return rs.IndexAllBckEx(r, sub, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in runes from front to end
|
||||||
|
func (rs Runes) IndexAll(r, sub []rune) int {
|
||||||
|
return rs.IndexAllEx(r, sub, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs Runes) IndexAllEx(r, sub []rune, fold bool) int {
|
||||||
|
for i := 0; i < len(r); i++ {
|
||||||
|
found := true
|
||||||
|
if len(r[i:]) < len(sub) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
for j := 0; j < len(sub); j++ {
|
||||||
|
if !rs.EqualRune(r[i+j], sub[j], fold) {
|
||||||
|
found = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) Index(r rune, rs []rune) int {
|
||||||
|
for i := 0; i < len(rs); i++ {
|
||||||
|
if rs[i] == r {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) ColorFilter(r []rune) []rune {
|
||||||
|
newr := make([]rune, 0, len(r))
|
||||||
|
for pos := 0; pos < len(r); pos++ {
|
||||||
|
if r[pos] == '\033' && r[pos+1] == '[' {
|
||||||
|
idx := runes.Index('m', r[pos+2:])
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pos += idx + 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newr = append(newr, r[pos])
|
||||||
|
}
|
||||||
|
return newr
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroWidth = []*unicode.RangeTable{
|
||||||
|
unicode.Mn,
|
||||||
|
unicode.Me,
|
||||||
|
unicode.Cc,
|
||||||
|
unicode.Cf,
|
||||||
|
}
|
||||||
|
|
||||||
|
var doubleWidth = []*unicode.RangeTable{
|
||||||
|
unicode.Han,
|
||||||
|
unicode.Hangul,
|
||||||
|
unicode.Hiragana,
|
||||||
|
unicode.Katakana,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) Width(r rune) int {
|
||||||
|
if r == '\t' {
|
||||||
|
return TabWidth
|
||||||
|
}
|
||||||
|
if unicode.IsOneOf(zeroWidth, r) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if unicode.IsOneOf(doubleWidth, r) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) WidthAll(r []rune) (length int) {
|
||||||
|
for i := 0; i < len(r); i++ {
|
||||||
|
length += runes.Width(r[i])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) Backspace(r []rune) []byte {
|
||||||
|
return bytes.Repeat([]byte{'\b'}, runes.WidthAll(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) Copy(r []rune) []rune {
|
||||||
|
n := make([]rune, len(r))
|
||||||
|
copy(n, r)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) HasPrefixFold(r, prefix []rune) bool {
|
||||||
|
if len(r) < len(prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return runes.EqualFold(r[:len(prefix)], prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) HasPrefix(r, prefix []rune) bool {
|
||||||
|
if len(r) < len(prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return runes.Equal(r[:len(prefix)], prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) Aggregate(candicate [][]rune) (same []rune, size int) {
|
||||||
|
for i := 0; i < len(candicate[0]); i++ {
|
||||||
|
for j := 0; j < len(candicate)-1; j++ {
|
||||||
|
if i >= len(candicate[j]) || i >= len(candicate[j+1]) {
|
||||||
|
goto aggregate
|
||||||
|
}
|
||||||
|
if candicate[j][i] != candicate[j+1][i] {
|
||||||
|
goto aggregate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size = i + 1
|
||||||
|
}
|
||||||
|
aggregate:
|
||||||
|
if size > 0 {
|
||||||
|
same = runes.Copy(candicate[0][:size])
|
||||||
|
for i := 0; i < len(candicate); i++ {
|
||||||
|
n := runes.Copy(candicate[i])
|
||||||
|
copy(n, n[size:])
|
||||||
|
candicate[i] = n[:len(n)-size]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Runes) TrimSpaceLeft(in []rune) []rune {
|
||||||
|
firstIndex := len(in)
|
||||||
|
for i, r := range in {
|
||||||
|
if unicode.IsSpace(r) == false {
|
||||||
|
firstIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in[firstIndex:]
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
// deprecated.
|
||||||
|
// see https://git.internal/re/readline/issues/43
|
||||||
|
// use git.internal/re/readline/runes.go
|
||||||
|
package runes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Equal(a, b []rune) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in runes from end to front
|
||||||
|
func IndexAllBck(r, sub []rune) int {
|
||||||
|
for i := len(r) - len(sub); i >= 0; i-- {
|
||||||
|
found := true
|
||||||
|
for j := 0; j < len(sub); j++ {
|
||||||
|
if r[i+j] != sub[j] {
|
||||||
|
found = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search in runes from front to end
|
||||||
|
func IndexAll(r, sub []rune) int {
|
||||||
|
for i := 0; i < len(r); i++ {
|
||||||
|
found := true
|
||||||
|
if len(r[i:]) < len(sub) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
for j := 0; j < len(sub); j++ {
|
||||||
|
if r[i+j] != sub[j] {
|
||||||
|
found = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func Index(r rune, rs []rune) int {
|
||||||
|
for i := 0; i < len(rs); i++ {
|
||||||
|
if rs[i] == r {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func ColorFilter(r []rune) []rune {
|
||||||
|
newr := make([]rune, 0, len(r))
|
||||||
|
for pos := 0; pos < len(r); pos++ {
|
||||||
|
if r[pos] == '\033' && r[pos+1] == '[' {
|
||||||
|
idx := Index('m', r[pos+2:])
|
||||||
|
if idx == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pos += idx + 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newr = append(newr, r[pos])
|
||||||
|
}
|
||||||
|
return newr
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroWidth = []*unicode.RangeTable{
|
||||||
|
unicode.Mn,
|
||||||
|
unicode.Me,
|
||||||
|
unicode.Cc,
|
||||||
|
unicode.Cf,
|
||||||
|
}
|
||||||
|
|
||||||
|
var doubleWidth = []*unicode.RangeTable{
|
||||||
|
unicode.Han,
|
||||||
|
unicode.Hangul,
|
||||||
|
unicode.Hiragana,
|
||||||
|
unicode.Katakana,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Width(r rune) int {
|
||||||
|
if unicode.IsOneOf(zeroWidth, r) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if unicode.IsOneOf(doubleWidth, r) {
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func WidthAll(r []rune) (length int) {
|
||||||
|
for i := 0; i < len(r); i++ {
|
||||||
|
length += Width(r[i])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func Backspace(r []rune) []byte {
|
||||||
|
return bytes.Repeat([]byte{'\b'}, WidthAll(r))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Copy(r []rune) []rune {
|
||||||
|
n := make([]rune, len(r))
|
||||||
|
copy(n, r)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasPrefix(r, prefix []rune) bool {
|
||||||
|
if len(r) < len(prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Equal(r[:len(prefix)], prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Aggregate(candicate [][]rune) (same []rune, size int) {
|
||||||
|
for i := 0; i < len(candicate[0]); i++ {
|
||||||
|
for j := 0; j < len(candicate)-1; j++ {
|
||||||
|
if i >= len(candicate[j]) || i >= len(candicate[j+1]) {
|
||||||
|
goto aggregate
|
||||||
|
}
|
||||||
|
if candicate[j][i] != candicate[j+1][i] {
|
||||||
|
goto aggregate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size = i + 1
|
||||||
|
}
|
||||||
|
aggregate:
|
||||||
|
if size > 0 {
|
||||||
|
same = Copy(candicate[0][:size])
|
||||||
|
for i := 0; i < len(candicate); i++ {
|
||||||
|
n := Copy(candicate[i])
|
||||||
|
copy(n, n[size:])
|
||||||
|
candicate[i] = n[:len(n)-size]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package runes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type twidth struct {
|
||||||
|
r []rune
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneWidth(t *testing.T) {
|
||||||
|
runes := []twidth{
|
||||||
|
{[]rune("☭"), 1},
|
||||||
|
{[]rune("a"), 1},
|
||||||
|
{[]rune("你"), 2},
|
||||||
|
{ColorFilter([]rune("☭\033[13;1m你")), 3},
|
||||||
|
}
|
||||||
|
for _, r := range runes {
|
||||||
|
if w := WidthAll(r.r); w != r.length {
|
||||||
|
t.Fatal("result not expect", r.r, r.length, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type tagg struct {
|
||||||
|
r [][]rune
|
||||||
|
e [][]rune
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggRunes(t *testing.T) {
|
||||||
|
runes := []tagg{
|
||||||
|
{
|
||||||
|
[][]rune{[]rune("ab"), []rune("a"), []rune("abc")},
|
||||||
|
[][]rune{[]rune("b"), []rune(""), []rune("bc")},
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")},
|
||||||
|
[][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")},
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")},
|
||||||
|
[][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")},
|
||||||
|
[][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")},
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, r := range runes {
|
||||||
|
same, off := Aggregate(r.r)
|
||||||
|
if off != r.length {
|
||||||
|
t.Fatal("result not expect", off)
|
||||||
|
}
|
||||||
|
if len(same) != off {
|
||||||
|
t.Fatal("result not expect", same)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r.r, r.e) {
|
||||||
|
t.Fatal("result not expect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type twidth struct {
|
||||||
|
r []rune
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRuneWidth(t *testing.T) {
|
||||||
|
rs := []twidth{
|
||||||
|
{[]rune("☭"), 1},
|
||||||
|
{[]rune("a"), 1},
|
||||||
|
{[]rune("你"), 2},
|
||||||
|
{runes.ColorFilter([]rune("☭\033[13;1m你")), 3},
|
||||||
|
}
|
||||||
|
for _, r := range rs {
|
||||||
|
if w := runes.WidthAll(r.r); w != r.length {
|
||||||
|
t.Fatal("result not expect", r.r, r.length, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type tagg struct {
|
||||||
|
r [][]rune
|
||||||
|
e [][]rune
|
||||||
|
length int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggRunes(t *testing.T) {
|
||||||
|
rs := []tagg{
|
||||||
|
{
|
||||||
|
[][]rune{[]rune("ab"), []rune("a"), []rune("abc")},
|
||||||
|
[][]rune{[]rune("b"), []rune(""), []rune("bc")},
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[][]rune{[]rune("addb"), []rune("ajkajsdf"), []rune("aasdfkc")},
|
||||||
|
[][]rune{[]rune("ddb"), []rune("jkajsdf"), []rune("asdfkc")},
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")},
|
||||||
|
[][]rune{[]rune("ddb"), []rune("ajksdf"), []rune("aasdfkc")},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[][]rune{[]rune("ddb"), []rune("ddajksdf"), []rune("ddaasdfkc")},
|
||||||
|
[][]rune{[]rune("b"), []rune("ajksdf"), []rune("aasdfkc")},
|
||||||
|
2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, r := range rs {
|
||||||
|
same, off := runes.Aggregate(r.r)
|
||||||
|
if off != r.length {
|
||||||
|
t.Fatal("result not expect", off)
|
||||||
|
}
|
||||||
|
if len(same) != off {
|
||||||
|
t.Fatal("result not expect", same)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r.r, r.e) {
|
||||||
|
t.Fatal("result not expect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
S_STATE_FOUND = iota
|
||||||
|
S_STATE_FAILING
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
S_DIR_BCK = iota
|
||||||
|
S_DIR_FWD
|
||||||
|
)
|
||||||
|
|
||||||
|
type opSearch struct {
|
||||||
|
inMode bool
|
||||||
|
state int
|
||||||
|
dir int
|
||||||
|
source *list.Element
|
||||||
|
w io.Writer
|
||||||
|
buf *RuneBuffer
|
||||||
|
data []rune
|
||||||
|
history *opHistory
|
||||||
|
cfg *Config
|
||||||
|
markStart int
|
||||||
|
markEnd int
|
||||||
|
width int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory, cfg *Config, width int) *opSearch {
|
||||||
|
return &opSearch{
|
||||||
|
w: w,
|
||||||
|
buf: buf,
|
||||||
|
cfg: cfg,
|
||||||
|
history: history,
|
||||||
|
width: width,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) OnWidthChange(newWidth int) {
|
||||||
|
o.width = newWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) IsSearchMode() bool {
|
||||||
|
return o.inMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) SearchBackspace() {
|
||||||
|
if len(o.data) > 0 {
|
||||||
|
o.data = o.data[:len(o.data)-1]
|
||||||
|
o.search(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) findHistoryBy(isNewSearch bool) (int, *list.Element) {
|
||||||
|
if o.dir == S_DIR_BCK {
|
||||||
|
return o.history.FindBck(isNewSearch, o.data, o.buf.idx)
|
||||||
|
}
|
||||||
|
return o.history.FindFwd(isNewSearch, o.data, o.buf.idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) search(isChange bool) bool {
|
||||||
|
if len(o.data) == 0 {
|
||||||
|
o.state = S_STATE_FOUND
|
||||||
|
o.SearchRefresh(-1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
idx, elem := o.findHistoryBy(isChange)
|
||||||
|
if elem == nil {
|
||||||
|
o.SearchRefresh(-2)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
o.history.current = elem
|
||||||
|
|
||||||
|
item := o.history.showItem(o.history.current.Value)
|
||||||
|
start, end := 0, 0
|
||||||
|
if o.dir == S_DIR_BCK {
|
||||||
|
start, end = idx, idx+len(o.data)
|
||||||
|
} else {
|
||||||
|
start, end = idx, idx+len(o.data)
|
||||||
|
idx += len(o.data)
|
||||||
|
}
|
||||||
|
o.buf.SetWithIdx(idx, item)
|
||||||
|
o.markStart, o.markEnd = start, end
|
||||||
|
o.SearchRefresh(idx)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) SearchChar(r rune) {
|
||||||
|
o.data = append(o.data, r)
|
||||||
|
o.search(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) SearchMode(dir int) bool {
|
||||||
|
if o.width == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
alreadyInMode := o.inMode
|
||||||
|
o.inMode = true
|
||||||
|
o.dir = dir
|
||||||
|
o.source = o.history.current
|
||||||
|
if alreadyInMode {
|
||||||
|
o.search(false)
|
||||||
|
} else {
|
||||||
|
o.SearchRefresh(-1)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) ExitSearchMode(revert bool) {
|
||||||
|
if revert {
|
||||||
|
o.history.current = o.source
|
||||||
|
o.buf.Set(o.history.showItem(o.history.current.Value))
|
||||||
|
}
|
||||||
|
o.markStart, o.markEnd = 0, 0
|
||||||
|
o.state = S_STATE_FOUND
|
||||||
|
o.inMode = false
|
||||||
|
o.source = nil
|
||||||
|
o.data = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opSearch) SearchRefresh(x int) {
|
||||||
|
if x == -2 {
|
||||||
|
o.state = S_STATE_FAILING
|
||||||
|
} else if x >= 0 {
|
||||||
|
o.state = S_STATE_FOUND
|
||||||
|
}
|
||||||
|
if x < 0 {
|
||||||
|
x = o.buf.idx
|
||||||
|
}
|
||||||
|
x = o.buf.CurrentWidth(x)
|
||||||
|
x += o.buf.PromptLen()
|
||||||
|
x = x % o.width
|
||||||
|
|
||||||
|
if o.markStart > 0 {
|
||||||
|
o.buf.SetStyle(o.markStart, o.markEnd, "4")
|
||||||
|
}
|
||||||
|
|
||||||
|
lineCnt := o.buf.CursorLineCount()
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
|
||||||
|
buf.WriteString("\033[J")
|
||||||
|
if o.state == S_STATE_FAILING {
|
||||||
|
buf.WriteString("failing ")
|
||||||
|
}
|
||||||
|
if o.dir == S_DIR_BCK {
|
||||||
|
buf.WriteString("bck")
|
||||||
|
} else if o.dir == S_DIR_FWD {
|
||||||
|
buf.WriteString("fwd")
|
||||||
|
}
|
||||||
|
buf.WriteString("-i-search: ")
|
||||||
|
buf.WriteString(string(o.data)) // keyword
|
||||||
|
buf.WriteString("\033[4m \033[0m") // _
|
||||||
|
fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev
|
||||||
|
if x > 0 {
|
||||||
|
fmt.Fprintf(buf, "\033[%dC", x) // move forward
|
||||||
|
}
|
||||||
|
o.w.Write(buf.Bytes())
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Stdin io.ReadCloser = os.Stdin
|
||||||
|
Stdout io.WriteCloser = os.Stdout
|
||||||
|
Stderr io.WriteCloser = os.Stderr
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
std *Instance
|
||||||
|
stdOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// global instance will not submit history automatic
|
||||||
|
func getInstance() *Instance {
|
||||||
|
stdOnce.Do(func() {
|
||||||
|
std, _ = NewEx(&Config{
|
||||||
|
DisableAutoSaveHistory: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return std
|
||||||
|
}
|
||||||
|
|
||||||
|
// let readline load history from filepath
|
||||||
|
// and try to persist history into disk
|
||||||
|
// set fp to "" to prevent readline persisting history to disk
|
||||||
|
// so the `AddHistory` will return nil error forever.
|
||||||
|
func SetHistoryPath(fp string) {
|
||||||
|
ins := getInstance()
|
||||||
|
cfg := ins.Config.Clone()
|
||||||
|
cfg.HistoryFile = fp
|
||||||
|
ins.SetConfig(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set auto completer to global instance
|
||||||
|
func SetAutoComplete(completer AutoCompleter) {
|
||||||
|
ins := getInstance()
|
||||||
|
cfg := ins.Config.Clone()
|
||||||
|
cfg.AutoComplete = completer
|
||||||
|
ins.SetConfig(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add history to global instance manually
|
||||||
|
// raise error only if `SetHistoryPath` is set with a non-empty path
|
||||||
|
func AddHistory(content string) error {
|
||||||
|
ins := getInstance()
|
||||||
|
return ins.SaveHistory(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Password(prompt string) ([]byte, error) {
|
||||||
|
ins := getInstance()
|
||||||
|
return ins.ReadPassword(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readline with global configs
|
||||||
|
func Line(prompt string) (string, error) {
|
||||||
|
ins := getInstance()
|
||||||
|
ins.SetPrompt(prompt)
|
||||||
|
return ins.Readline()
|
||||||
|
}
|
||||||
|
|
||||||
|
type CancelableStdin struct {
|
||||||
|
r io.Reader
|
||||||
|
mutex sync.Mutex
|
||||||
|
stop chan struct{}
|
||||||
|
closed int32
|
||||||
|
notify chan struct{}
|
||||||
|
data []byte
|
||||||
|
read int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCancelableStdin(r io.Reader) *CancelableStdin {
|
||||||
|
c := &CancelableStdin{
|
||||||
|
r: r,
|
||||||
|
notify: make(chan struct{}),
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go c.ioloop()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelableStdin) ioloop() {
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.notify:
|
||||||
|
c.read, c.err = c.r.Read(c.data)
|
||||||
|
select {
|
||||||
|
case c.notify <- struct{}{}:
|
||||||
|
case <-c.stop:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
case <-c.stop:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelableStdin) Read(b []byte) (n int, err error) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
if atomic.LoadInt32(&c.closed) == 1 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
c.data = b
|
||||||
|
select {
|
||||||
|
case c.notify <- struct{}{}:
|
||||||
|
case <-c.stop:
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-c.notify:
|
||||||
|
return c.read, c.err
|
||||||
|
case <-c.stop:
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CancelableStdin) Close() error {
|
||||||
|
if atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Stdin = NewRawReader()
|
||||||
|
Stdout = NewANSIWriter(Stdout)
|
||||||
|
Stderr = NewANSIWriter(Stderr)
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
// 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 aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris
|
||||||
|
|
||||||
|
// Package terminal provides support functions for dealing with terminals, as
|
||||||
|
// commonly found on UNIX systems.
|
||||||
|
//
|
||||||
|
// Putting a terminal into raw mode is the most common requirement:
|
||||||
|
//
|
||||||
|
// oldState, err := terminal.MakeRaw(0)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer terminal.Restore(0, oldState)
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// State contains the state of a terminal.
|
||||||
|
type State struct {
|
||||||
|
termios Termios
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
_, err := getTermios(fd)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
var oldState State
|
||||||
|
|
||||||
|
if termios, err := getTermios(fd); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
oldState.termios = *termios
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := oldState.termios
|
||||||
|
// This attempts to replicate the behaviour documented for cfmakeraw in
|
||||||
|
// the termios(3) manpage.
|
||||||
|
newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
|
||||||
|
// newState.Oflag &^= syscall.OPOST
|
||||||
|
newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
|
||||||
|
newState.Cflag &^= syscall.CSIZE | syscall.PARENB
|
||||||
|
newState.Cflag |= syscall.CS8
|
||||||
|
|
||||||
|
newState.Cc[syscall.VMIN] = 1
|
||||||
|
newState.Cc[syscall.VTIME] = 0
|
||||||
|
|
||||||
|
return &oldState, setTermios(fd, &newState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
termios, err := getTermios(fd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{termios: *termios}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func restoreTerm(fd int, state *State) error {
|
||||||
|
return setTermios(fd, &state.termios)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// returned does not include the \n.
|
||||||
|
func ReadPassword(fd int) ([]byte, error) {
|
||||||
|
oldState, err := getTermios(fd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := oldState
|
||||||
|
newState.Lflag &^= syscall.ECHO
|
||||||
|
newState.Lflag |= syscall.ICANON | syscall.ISIG
|
||||||
|
newState.Iflag |= syscall.ICRNL
|
||||||
|
if err := setTermios(fd, newState); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
setTermios(fd, oldState)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var buf [16]byte
|
||||||
|
var ret []byte
|
||||||
|
for {
|
||||||
|
n, err := syscall.Read(fd, buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if buf[n-1] == '\n' {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
ret = append(ret, buf[:n]...)
|
||||||
|
if n < len(buf) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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 darwin dragonfly freebsd netbsd openbsd
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTermios(fd int) (*Termios, error) {
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These constants are declared here, rather than importing
|
||||||
|
// them from the syscall package as some syscall packages, even
|
||||||
|
// on linux, for example gccgo, do not declare them.
|
||||||
|
const ioctlReadTermios = 0x5401 // syscall.TCGETS
|
||||||
|
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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
// 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 windows
|
||||||
|
|
||||||
|
// Package terminal provides support functions for dealing with terminals, as
|
||||||
|
// commonly found on UNIX systems.
|
||||||
|
//
|
||||||
|
// Putting a terminal into raw mode is the most common requirement:
|
||||||
|
//
|
||||||
|
// oldState, err := terminal.MakeRaw(0)
|
||||||
|
// if err != nil {
|
||||||
|
// panic(err)
|
||||||
|
// }
|
||||||
|
// defer terminal.Restore(0, oldState)
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
enableLineInput = 2
|
||||||
|
enableEchoInput = 4
|
||||||
|
enableProcessedInput = 1
|
||||||
|
enableWindowInput = 8
|
||||||
|
enableMouseInput = 16
|
||||||
|
enableInsertMode = 32
|
||||||
|
enableQuickEditMode = 64
|
||||||
|
enableExtendedFlags = 128
|
||||||
|
enableAutoPosition = 256
|
||||||
|
enableProcessedOutput = 1
|
||||||
|
enableWrapAtEolOutput = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
|
||||||
|
var (
|
||||||
|
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||||
|
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
|
||||||
|
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
coord struct {
|
||||||
|
x short
|
||||||
|
y short
|
||||||
|
}
|
||||||
|
smallRect struct {
|
||||||
|
left short
|
||||||
|
top short
|
||||||
|
right short
|
||||||
|
bottom short
|
||||||
|
}
|
||||||
|
consoleScreenBufferInfo struct {
|
||||||
|
size coord
|
||||||
|
cursorPosition coord
|
||||||
|
attributes word
|
||||||
|
window smallRect
|
||||||
|
maximumWindowSize coord
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
mode uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
func IsTerminal(fd int) bool {
|
||||||
|
var st uint32
|
||||||
|
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||||
|
return r != 0 && e == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
|
func MakeRaw(fd int) (*State, error) {
|
||||||
|
var st uint32
|
||||||
|
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||||
|
if e != 0 {
|
||||||
|
return nil, error(e)
|
||||||
|
}
|
||||||
|
raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput)
|
||||||
|
_, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0)
|
||||||
|
if e != 0 {
|
||||||
|
return nil, error(e)
|
||||||
|
}
|
||||||
|
return &State{st}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetState returns the current state of a terminal which may be useful to
|
||||||
|
// restore the terminal after a signal.
|
||||||
|
func GetState(fd int) (*State, error) {
|
||||||
|
var st uint32
|
||||||
|
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||||
|
if e != 0 {
|
||||||
|
return nil, error(e)
|
||||||
|
}
|
||||||
|
return &State{st}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the terminal connected to the given file descriptor to a
|
||||||
|
// previous state.
|
||||||
|
func restoreTerm(fd int, state *State) error {
|
||||||
|
_, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSize returns the dimensions of the given terminal.
|
||||||
|
func GetSize(fd int) (width, height int, err error) {
|
||||||
|
var info consoleScreenBufferInfo
|
||||||
|
_, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0)
|
||||||
|
if e != 0 {
|
||||||
|
return 0, 0, error(e)
|
||||||
|
}
|
||||||
|
return int(info.size.x), int(info.size.y), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// returned does not include the \n.
|
||||||
|
func ReadPassword(fd int) ([]byte, error) {
|
||||||
|
var st uint32
|
||||||
|
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||||
|
if e != 0 {
|
||||||
|
return nil, error(e)
|
||||||
|
}
|
||||||
|
old := st
|
||||||
|
|
||||||
|
st &^= (enableEchoInput)
|
||||||
|
st |= (enableProcessedInput | enableLineInput | enableProcessedOutput)
|
||||||
|
_, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0)
|
||||||
|
if e != 0 {
|
||||||
|
return nil, error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var buf [16]byte
|
||||||
|
var ret []byte
|
||||||
|
for {
|
||||||
|
n, err := syscall.Read(syscall.Handle(fd), buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if buf[n-1] == '\n' {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
if n > 0 && buf[n-1] == '\r' {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
ret = append(ret, buf[:n]...)
|
||||||
|
if n < len(buf) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Terminal struct {
|
||||||
|
m sync.Mutex
|
||||||
|
cfg *Config
|
||||||
|
outchan chan rune
|
||||||
|
closed int32
|
||||||
|
stopChan chan struct{}
|
||||||
|
kickChan chan struct{}
|
||||||
|
wg sync.WaitGroup
|
||||||
|
isReading int32
|
||||||
|
sleeping int32
|
||||||
|
|
||||||
|
sizeChan chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTerminal(cfg *Config) (*Terminal, error) {
|
||||||
|
if err := cfg.Init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := &Terminal{
|
||||||
|
cfg: cfg,
|
||||||
|
kickChan: make(chan struct{}, 1),
|
||||||
|
outchan: make(chan rune),
|
||||||
|
stopChan: make(chan struct{}, 1),
|
||||||
|
sizeChan: make(chan string, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
go t.ioloop()
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SleepToResume will sleep myself, and return only if I'm resumed.
|
||||||
|
func (t *Terminal) SleepToResume() {
|
||||||
|
if !atomic.CompareAndSwapInt32(&t.sleeping, 0, 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer atomic.StoreInt32(&t.sleeping, 0)
|
||||||
|
|
||||||
|
t.ExitRawMode()
|
||||||
|
ch := WaitForResume()
|
||||||
|
SuspendMe()
|
||||||
|
<-ch
|
||||||
|
t.EnterRawMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) EnterRawMode() (err error) {
|
||||||
|
return t.cfg.FuncMakeRaw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) ExitRawMode() (err error) {
|
||||||
|
return t.cfg.FuncExitRaw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Write(b []byte) (int, error) {
|
||||||
|
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 {
|
||||||
|
left int
|
||||||
|
top int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) GetOffset(f func(offset string)) {
|
||||||
|
go func() {
|
||||||
|
f(<-t.sizeChan)
|
||||||
|
}()
|
||||||
|
t.Write([]byte("\033[6n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Print(s string) {
|
||||||
|
fmt.Fprintf(t.cfg.Stdout, "%s", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) PrintRune(r rune) {
|
||||||
|
fmt.Fprintf(t.cfg.Stdout, "%c", r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Readline() *Operation {
|
||||||
|
return NewOperation(t, t.cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return rune(0) if meet EOF
|
||||||
|
func (t *Terminal) ReadRune() rune {
|
||||||
|
ch, ok := <-t.outchan
|
||||||
|
if !ok {
|
||||||
|
return rune(0)
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) IsReading() bool {
|
||||||
|
return atomic.LoadInt32(&t.isReading) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) KickRead() {
|
||||||
|
select {
|
||||||
|
case t.kickChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) ioloop() {
|
||||||
|
t.wg.Add(1)
|
||||||
|
defer func() {
|
||||||
|
t.wg.Done()
|
||||||
|
close(t.outchan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var (
|
||||||
|
isEscape bool
|
||||||
|
isEscapeEx bool
|
||||||
|
isEscapeSS3 bool
|
||||||
|
expectNextChar bool
|
||||||
|
)
|
||||||
|
|
||||||
|
buf := bufio.NewReader(t.getStdin())
|
||||||
|
for {
|
||||||
|
if !expectNextChar {
|
||||||
|
atomic.StoreInt32(&t.isReading, 0)
|
||||||
|
select {
|
||||||
|
case <-t.kickChan:
|
||||||
|
atomic.StoreInt32(&t.isReading, 1)
|
||||||
|
case <-t.stopChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expectNextChar = false
|
||||||
|
r, _, err := buf.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "interrupted system call") {
|
||||||
|
expectNextChar = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if isEscape {
|
||||||
|
isEscape = false
|
||||||
|
if r == CharEscapeEx {
|
||||||
|
// ^][
|
||||||
|
expectNextChar = true
|
||||||
|
isEscapeEx = true
|
||||||
|
continue
|
||||||
|
} else if r == CharO {
|
||||||
|
// ^]O
|
||||||
|
expectNextChar = true
|
||||||
|
isEscapeSS3 = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r = escapeKey(r, buf)
|
||||||
|
} else if isEscapeEx {
|
||||||
|
isEscapeEx = false
|
||||||
|
if key := readEscKey(r, buf); key != nil {
|
||||||
|
r = escapeExKey(key)
|
||||||
|
// offset
|
||||||
|
if key.typ == 'R' {
|
||||||
|
if _, _, ok := key.Get2(); ok {
|
||||||
|
select {
|
||||||
|
case t.sizeChan <- key.attr:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expectNextChar = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if r == 0 {
|
||||||
|
expectNextChar = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if isEscapeSS3 {
|
||||||
|
isEscapeSS3 = false
|
||||||
|
if key := readEscKey(r, buf); key != nil {
|
||||||
|
r = escapeSS3Key(key)
|
||||||
|
}
|
||||||
|
if r == 0 {
|
||||||
|
expectNextChar = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expectNextChar = true
|
||||||
|
switch r {
|
||||||
|
case CharEsc:
|
||||||
|
if t.cfg.VimMode {
|
||||||
|
t.outchan <- r
|
||||||
|
break
|
||||||
|
}
|
||||||
|
isEscape = true
|
||||||
|
case CharInterrupt, CharEnter, CharCtrlJ, CharDelete:
|
||||||
|
expectNextChar = false
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
t.outchan <- r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Bell() {
|
||||||
|
fmt.Fprintf(t, "%c", CharBell)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Terminal) Close() error {
|
||||||
|
if atomic.SwapInt32(&t.closed, 1) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if closer, ok := t.cfg.Stdin.(io.Closer); ok {
|
||||||
|
closer.Close()
|
||||||
|
}
|
||||||
|
close(t.stopChan)
|
||||||
|
t.wg.Wait()
|
||||||
|
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 {
|
||||||
|
if err := c.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.m.Lock()
|
||||||
|
t.cfg = c
|
||||||
|
t.m.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,311 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
isWindows = false
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CharLineStart = 1
|
||||||
|
CharBackward = 2
|
||||||
|
CharInterrupt = 3
|
||||||
|
CharDelete = 4
|
||||||
|
CharLineEnd = 5
|
||||||
|
CharForward = 6
|
||||||
|
CharBell = 7
|
||||||
|
CharCtrlH = 8
|
||||||
|
CharTab = 9
|
||||||
|
CharCtrlJ = 10
|
||||||
|
CharKill = 11
|
||||||
|
CharCtrlL = 12
|
||||||
|
CharEnter = 13
|
||||||
|
CharNext = 14
|
||||||
|
CharPrev = 16
|
||||||
|
CharBckSearch = 18
|
||||||
|
CharFwdSearch = 19
|
||||||
|
CharTranspose = 20
|
||||||
|
CharCtrlU = 21
|
||||||
|
CharCtrlW = 23
|
||||||
|
CharCtrlY = 25
|
||||||
|
CharCtrlZ = 26
|
||||||
|
CharEsc = 27
|
||||||
|
CharO = 79
|
||||||
|
CharEscapeEx = 91
|
||||||
|
CharBackspace = 127
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MetaBackward rune = -iota - 1
|
||||||
|
MetaForward
|
||||||
|
MetaDelete
|
||||||
|
MetaBackspace
|
||||||
|
MetaTranspose
|
||||||
|
)
|
||||||
|
|
||||||
|
// WaitForResume need to call before current process got suspend.
|
||||||
|
// It will run a ticker until a long duration is occurs,
|
||||||
|
// which means this process is resumed.
|
||||||
|
func WaitForResume() chan struct{} {
|
||||||
|
ch := make(chan struct{})
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(10 * time.Millisecond)
|
||||||
|
t := time.Now()
|
||||||
|
wg.Done()
|
||||||
|
for {
|
||||||
|
now := <-ticker.C
|
||||||
|
if now.Sub(t) > 100*time.Millisecond {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t = now
|
||||||
|
}
|
||||||
|
ticker.Stop()
|
||||||
|
ch <- struct{}{}
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func Restore(fd int, state *State) error {
|
||||||
|
err := restoreTerm(fd, state)
|
||||||
|
if err != nil {
|
||||||
|
// errno 0 means everything is ok :)
|
||||||
|
if err.Error() == "errno 0" {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPrintable(key rune) bool {
|
||||||
|
isInSurrogateArea := key >= 0xd800 && key <= 0xdbff
|
||||||
|
return key >= 32 && !isInSurrogateArea
|
||||||
|
}
|
||||||
|
|
||||||
|
// translate Esc[X
|
||||||
|
func escapeExKey(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
|
||||||
|
case '~':
|
||||||
|
if key.attr == "3" {
|
||||||
|
r = CharDelete
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
attr string
|
||||||
|
typ rune
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *escapeKeyPair) Get2() (int, int, bool) {
|
||||||
|
sp := strings.Split(e.attr, ";")
|
||||||
|
if len(sp) < 2 {
|
||||||
|
return -1, -1, false
|
||||||
|
}
|
||||||
|
s1, err := strconv.Atoi(sp[0])
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, false
|
||||||
|
}
|
||||||
|
s2, err := strconv.Atoi(sp[1])
|
||||||
|
if err != nil {
|
||||||
|
return -1, -1, false
|
||||||
|
}
|
||||||
|
return s1, s2, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func readEscKey(r rune, reader *bufio.Reader) *escapeKeyPair {
|
||||||
|
p := escapeKeyPair{}
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
for {
|
||||||
|
if r == ';' {
|
||||||
|
} else if unicode.IsNumber(r) {
|
||||||
|
} else {
|
||||||
|
p.typ = r
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf.WriteRune(r)
|
||||||
|
r, _, _ = reader.ReadRune()
|
||||||
|
}
|
||||||
|
p.attr = buf.String()
|
||||||
|
return &p
|
||||||
|
}
|
||||||
|
|
||||||
|
// translate EscX to Meta+X
|
||||||
|
func escapeKey(r rune, reader *bufio.Reader) rune {
|
||||||
|
switch r {
|
||||||
|
case 'b':
|
||||||
|
r = MetaBackward
|
||||||
|
case 'f':
|
||||||
|
r = MetaForward
|
||||||
|
case 'd':
|
||||||
|
r = MetaDelete
|
||||||
|
case CharTranspose:
|
||||||
|
r = MetaTranspose
|
||||||
|
case CharBackspace:
|
||||||
|
r = MetaBackspace
|
||||||
|
case 'O':
|
||||||
|
d, _, _ := reader.ReadRune()
|
||||||
|
switch d {
|
||||||
|
case 'H':
|
||||||
|
r = CharLineStart
|
||||||
|
case 'F':
|
||||||
|
r = CharLineEnd
|
||||||
|
default:
|
||||||
|
reader.UnreadRune()
|
||||||
|
}
|
||||||
|
case CharEsc:
|
||||||
|
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitByLine(start, screenWidth int, rs []rune) []string {
|
||||||
|
var ret []string
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
currentWidth := start
|
||||||
|
for _, r := range rs {
|
||||||
|
w := runes.Width(r)
|
||||||
|
currentWidth += w
|
||||||
|
buf.WriteRune(r)
|
||||||
|
if currentWidth >= screenWidth {
|
||||||
|
ret = append(ret, buf.String())
|
||||||
|
buf.Reset()
|
||||||
|
currentWidth = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret = append(ret, buf.String())
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate how many lines for N character
|
||||||
|
func LineCount(screenWidth, w int) int {
|
||||||
|
r := w / screenWidth
|
||||||
|
if w%screenWidth != 0 {
|
||||||
|
r++
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsWordBreak(i rune) bool {
|
||||||
|
switch {
|
||||||
|
case i >= 'a' && i <= 'z':
|
||||||
|
case i >= 'A' && i <= 'Z':
|
||||||
|
case i >= '0' && i <= '9':
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInt(s []string, def int) int {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
c, err := strconv.Atoi(s[0])
|
||||||
|
if err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawMode struct {
|
||||||
|
state *State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawMode) Enter() (err error) {
|
||||||
|
r.state, err = MakeRaw(GetStdin())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RawMode) Exit() error {
|
||||||
|
if r.state == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Restore(GetStdin(), r.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func sleep(n int) {
|
||||||
|
Debug(n)
|
||||||
|
time.Sleep(2000 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// print a linked list to Debug()
|
||||||
|
func debugList(l *list.List) {
|
||||||
|
idx := 0
|
||||||
|
for e := l.Front(); e != nil; e = e.Next() {
|
||||||
|
Debug(idx, fmt.Sprintf("%+v", e.Value))
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// append log info to another file
|
||||||
|
func Debug(o ...interface{}) {
|
||||||
|
f, _ := os.OpenFile("debug.tmp", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||||
|
fmt.Fprintln(f, o...)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package readline
|
|
@ -0,0 +1,83 @@
|
||||||
|
// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd os400 solaris
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type winsize struct {
|
||||||
|
Row uint16
|
||||||
|
Col uint16
|
||||||
|
Xpixel uint16
|
||||||
|
Ypixel uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuspendMe use to send suspend signal to myself, when we in the raw mode.
|
||||||
|
// For OSX it need to send to parent's pid
|
||||||
|
// For Linux it need to send to myself
|
||||||
|
func SuspendMe() {
|
||||||
|
p, _ := os.FindProcess(os.Getppid())
|
||||||
|
p.Signal(syscall.SIGTSTP)
|
||||||
|
p, _ = os.FindProcess(os.Getpid())
|
||||||
|
p.Signal(syscall.SIGTSTP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get width of the terminal
|
||||||
|
func getWidth(stdoutFd int) int {
|
||||||
|
cols, _, err := GetSize(stdoutFd)
|
||||||
|
if err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return cols
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetScreenWidth() int {
|
||||||
|
w := getWidth(syscall.Stdout)
|
||||||
|
if w < 0 {
|
||||||
|
w = getWidth(syscall.Stderr)
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearScreen clears the console screen
|
||||||
|
func ClearScreen(w io.Writer) (int, error) {
|
||||||
|
return w.Write([]byte("\033[H"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultIsTerminal() bool {
|
||||||
|
return IsTerminal(syscall.Stdin) && (IsTerminal(syscall.Stdout) || IsTerminal(syscall.Stderr))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStdin() int {
|
||||||
|
return syscall.Stdin
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
var (
|
||||||
|
widthChange sync.Once
|
||||||
|
widthChangeCallback func()
|
||||||
|
)
|
||||||
|
|
||||||
|
func DefaultOnWidthChanged(f func()) {
|
||||||
|
widthChangeCallback = f
|
||||||
|
widthChange.Do(func() {
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, syscall.SIGWINCH)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
_, ok := <-ch
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
widthChangeCallback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SuspendMe() {
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStdin() int {
|
||||||
|
return int(syscall.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
isWindows = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// get width of the terminal
|
||||||
|
func GetScreenWidth() int {
|
||||||
|
info, _ := GetConsoleScreenBufferInfo()
|
||||||
|
if info == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return int(info.dwSize.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearScreen clears the console screen
|
||||||
|
func ClearScreen(_ io.Writer) error {
|
||||||
|
return SetConsoleCursorPosition(&_COORD{0, 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultIsTerminal() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultOnWidthChanged(func()) {
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
package readline
|
||||||
|
|
||||||
|
const (
|
||||||
|
VIM_NORMAL = iota
|
||||||
|
VIM_INSERT
|
||||||
|
VIM_VISUAL
|
||||||
|
)
|
||||||
|
|
||||||
|
type opVim struct {
|
||||||
|
cfg *Config
|
||||||
|
op *Operation
|
||||||
|
vimMode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVimMode(op *Operation) *opVim {
|
||||||
|
ov := &opVim{
|
||||||
|
cfg: op.cfg,
|
||||||
|
op: op,
|
||||||
|
}
|
||||||
|
ov.SetVimMode(ov.cfg.VimMode)
|
||||||
|
return ov
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) SetVimMode(on bool) {
|
||||||
|
if o.cfg.VimMode && !on { // turn off
|
||||||
|
o.ExitVimMode()
|
||||||
|
}
|
||||||
|
o.cfg.VimMode = on
|
||||||
|
o.vimMode = VIM_INSERT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) ExitVimMode() {
|
||||||
|
o.vimMode = VIM_INSERT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) IsEnableVimMode() bool {
|
||||||
|
return o.cfg.VimMode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) {
|
||||||
|
rb := o.op.buf
|
||||||
|
handled = true
|
||||||
|
switch r {
|
||||||
|
case 'h':
|
||||||
|
t = CharBackward
|
||||||
|
case 'j':
|
||||||
|
t = CharNext
|
||||||
|
case 'k':
|
||||||
|
t = CharPrev
|
||||||
|
case 'l':
|
||||||
|
t = CharForward
|
||||||
|
case '0', '^':
|
||||||
|
rb.MoveToLineStart()
|
||||||
|
case '$':
|
||||||
|
rb.MoveToLineEnd()
|
||||||
|
case 'x':
|
||||||
|
rb.Delete()
|
||||||
|
if rb.IsCursorInEnd() {
|
||||||
|
rb.MoveBackward()
|
||||||
|
}
|
||||||
|
case 'r':
|
||||||
|
rb.Replace(readNext())
|
||||||
|
case 'd':
|
||||||
|
next := readNext()
|
||||||
|
switch next {
|
||||||
|
case 'd':
|
||||||
|
rb.Erase()
|
||||||
|
case 'w':
|
||||||
|
rb.DeleteWord()
|
||||||
|
case 'h':
|
||||||
|
rb.Backspace()
|
||||||
|
case 'l':
|
||||||
|
rb.Delete()
|
||||||
|
}
|
||||||
|
case 'p':
|
||||||
|
rb.Yank()
|
||||||
|
case 'b', 'B':
|
||||||
|
rb.MoveToPrevWord()
|
||||||
|
case 'w', 'W':
|
||||||
|
rb.MoveToNextWord()
|
||||||
|
case 'e', 'E':
|
||||||
|
rb.MoveToEndWord()
|
||||||
|
case 'f', 'F', 't', 'T':
|
||||||
|
next := readNext()
|
||||||
|
prevChar := r == 't' || r == 'T'
|
||||||
|
reverse := r == 'F' || r == 'T'
|
||||||
|
switch next {
|
||||||
|
case CharEsc:
|
||||||
|
default:
|
||||||
|
rb.MoveTo(next, prevChar, reverse)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return r, false
|
||||||
|
}
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) {
|
||||||
|
rb := o.op.buf
|
||||||
|
handled = true
|
||||||
|
switch r {
|
||||||
|
case 'i':
|
||||||
|
case 'I':
|
||||||
|
rb.MoveToLineStart()
|
||||||
|
case 'a':
|
||||||
|
rb.MoveForward()
|
||||||
|
case 'A':
|
||||||
|
rb.MoveToLineEnd()
|
||||||
|
case 's':
|
||||||
|
rb.Delete()
|
||||||
|
case 'S':
|
||||||
|
rb.Erase()
|
||||||
|
case 'c':
|
||||||
|
next := readNext()
|
||||||
|
switch next {
|
||||||
|
case 'c':
|
||||||
|
rb.Erase()
|
||||||
|
case 'w':
|
||||||
|
rb.DeleteWord()
|
||||||
|
case 'h':
|
||||||
|
rb.Backspace()
|
||||||
|
case 'l':
|
||||||
|
rb.Delete()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return r, false
|
||||||
|
}
|
||||||
|
|
||||||
|
o.EnterVimInsertMode()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) {
|
||||||
|
switch r {
|
||||||
|
case CharEnter, CharInterrupt:
|
||||||
|
o.ExitVimMode()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, handled := o.handleVimNormalMovement(r, readNext); handled {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid operation
|
||||||
|
o.op.t.Bell()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) EnterVimInsertMode() {
|
||||||
|
o.vimMode = VIM_INSERT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) ExitVimInsertMode() {
|
||||||
|
o.vimMode = VIM_NORMAL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opVim) HandleVim(r rune, readNext func() rune) rune {
|
||||||
|
if o.vimMode == VIM_NORMAL {
|
||||||
|
return o.HandleVimNormal(r, readNext)
|
||||||
|
}
|
||||||
|
if r == CharEsc {
|
||||||
|
o.ExitVimInsertMode()
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o.vimMode {
|
||||||
|
case VIM_INSERT:
|
||||||
|
return r
|
||||||
|
case VIM_VISUAL:
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package readline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel = NewKernel()
|
||||||
|
stdout = uintptr(syscall.Stdout)
|
||||||
|
stdin = uintptr(syscall.Stdin)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Kernel struct {
|
||||||
|
SetConsoleCursorPosition,
|
||||||
|
SetConsoleTextAttribute,
|
||||||
|
FillConsoleOutputCharacterW,
|
||||||
|
FillConsoleOutputAttribute,
|
||||||
|
ReadConsoleInputW,
|
||||||
|
GetConsoleScreenBufferInfo,
|
||||||
|
GetConsoleCursorInfo,
|
||||||
|
GetStdHandle CallFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type short int16
|
||||||
|
type word uint16
|
||||||
|
type dword uint32
|
||||||
|
type wchar uint16
|
||||||
|
|
||||||
|
type _COORD struct {
|
||||||
|
x short
|
||||||
|
y short
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *_COORD) ptr() uintptr {
|
||||||
|
return uintptr(*(*int32)(unsafe.Pointer(c)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
EVENT_KEY = 0x0001
|
||||||
|
EVENT_MOUSE = 0x0002
|
||||||
|
EVENT_WINDOW_BUFFER_SIZE = 0x0004
|
||||||
|
EVENT_MENU = 0x0008
|
||||||
|
EVENT_FOCUS = 0x0010
|
||||||
|
)
|
||||||
|
|
||||||
|
type _KEY_EVENT_RECORD struct {
|
||||||
|
bKeyDown int32
|
||||||
|
wRepeatCount word
|
||||||
|
wVirtualKeyCode word
|
||||||
|
wVirtualScanCode word
|
||||||
|
unicodeChar wchar
|
||||||
|
dwControlKeyState dword
|
||||||
|
}
|
||||||
|
|
||||||
|
// KEY_EVENT_RECORD KeyEvent;
|
||||||
|
// MOUSE_EVENT_RECORD MouseEvent;
|
||||||
|
// WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
|
||||||
|
// MENU_EVENT_RECORD MenuEvent;
|
||||||
|
// FOCUS_EVENT_RECORD FocusEvent;
|
||||||
|
type _INPUT_RECORD struct {
|
||||||
|
EventType word
|
||||||
|
Padding uint16
|
||||||
|
Event [16]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type _CONSOLE_SCREEN_BUFFER_INFO struct {
|
||||||
|
dwSize _COORD
|
||||||
|
dwCursorPosition _COORD
|
||||||
|
wAttributes word
|
||||||
|
srWindow _SMALL_RECT
|
||||||
|
dwMaximumWindowSize _COORD
|
||||||
|
}
|
||||||
|
|
||||||
|
type _SMALL_RECT struct {
|
||||||
|
left short
|
||||||
|
top short
|
||||||
|
right short
|
||||||
|
bottom short
|
||||||
|
}
|
||||||
|
|
||||||
|
type _CONSOLE_CURSOR_INFO struct {
|
||||||
|
dwSize dword
|
||||||
|
bVisible bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CallFunc func(u ...uintptr) error
|
||||||
|
|
||||||
|
func NewKernel() *Kernel {
|
||||||
|
k := &Kernel{}
|
||||||
|
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
v := reflect.ValueOf(k).Elem()
|
||||||
|
t := v.Type()
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
name := t.Field(i).Name
|
||||||
|
f := kernel32.NewProc(name)
|
||||||
|
v.Field(i).Set(reflect.ValueOf(k.Wrap(f)))
|
||||||
|
}
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kernel) Wrap(p *syscall.LazyProc) CallFunc {
|
||||||
|
return func(args ...uintptr) error {
|
||||||
|
var r0 uintptr
|
||||||
|
var e1 syscall.Errno
|
||||||
|
size := uintptr(len(args))
|
||||||
|
if len(args) <= 3 {
|
||||||
|
buf := make([]uintptr, 3)
|
||||||
|
copy(buf, args)
|
||||||
|
r0, _, e1 = syscall.Syscall(p.Addr(), size,
|
||||||
|
buf[0], buf[1], buf[2])
|
||||||
|
} else {
|
||||||
|
buf := make([]uintptr, 6)
|
||||||
|
copy(buf, args)
|
||||||
|
r0, _, e1 = syscall.Syscall6(p.Addr(), size,
|
||||||
|
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(r0) == 0 {
|
||||||
|
if e1 != 0 {
|
||||||
|
return error(e1)
|
||||||
|
} else {
|
||||||
|
return syscall.EINVAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConsoleScreenBufferInfo() (*_CONSOLE_SCREEN_BUFFER_INFO, error) {
|
||||||
|
t := new(_CONSOLE_SCREEN_BUFFER_INFO)
|
||||||
|
err := kernel.GetConsoleScreenBufferInfo(
|
||||||
|
stdout,
|
||||||
|
uintptr(unsafe.Pointer(t)),
|
||||||
|
)
|
||||||
|
return t, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetConsoleCursorInfo() (*_CONSOLE_CURSOR_INFO, error) {
|
||||||
|
t := new(_CONSOLE_CURSOR_INFO)
|
||||||
|
err := kernel.GetConsoleCursorInfo(stdout, uintptr(unsafe.Pointer(t)))
|
||||||
|
return t, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetConsoleCursorPosition(c *_COORD) error {
|
||||||
|
return kernel.SetConsoleCursorPosition(stdout, c.ptr())
|
||||||
|
}
|
Loading…
Reference in New Issue