support double-width-char & colorful prompt

This commit is contained in:
Cheney 2015-09-25 12:45:39 +08:00
parent af66dc48f7
commit f179b24304
5 changed files with 97 additions and 25 deletions

View File

@ -18,7 +18,7 @@ bye: quit
func main() { func main() {
l, err := readline.NewEx(&readline.Config{ l, err := readline.NewEx(&readline.Config{
Prompt: "home » ", Prompt: "home \033[31m»\033[0m ",
HistoryFile: "/tmp/readline.tmp", HistoryFile: "/tmp/readline.tmp",
}) })
if err != nil { if err != nil {

View File

@ -20,8 +20,12 @@ func NewRuneBuffer(w io.Writer, prompt string) *RuneBuffer {
return rb return rb
} }
func (r *RuneBuffer) CurrentWidth(x int) int {
return RunesWidth(r.buf[:x])
}
func (r *RuneBuffer) PromptLen() int { func (r *RuneBuffer) PromptLen() int {
return RunesWidth(r.prompt) return RunesWidth(RunesColorFilter(r.prompt))
} }
func (r *RuneBuffer) Runes() []rune { func (r *RuneBuffer) Runes() []rune {
@ -221,7 +225,7 @@ func (r *RuneBuffer) Output() []byte {
buf.WriteString(string(r.prompt)) buf.WriteString(string(r.prompt))
buf.Write([]byte(string(r.buf))) buf.Write([]byte(string(r.buf)))
if len(r.buf) > r.idx { if len(r.buf) > r.idx {
buf.Write(bytes.Repeat([]byte{'\b'}, len(r.buf)-r.idx)) buf.Write(bytes.Repeat([]byte{'\b'}, RunesWidth(r.buf[r.idx:])))
} }
return buf.Bytes() return buf.Bytes()
} }
@ -248,29 +252,29 @@ func (r *RuneBuffer) Reset() []rune {
return ret return ret
} }
func (r *RuneBuffer) calWidth(m int) int {
if m > 0 {
return RunesWidth(r.buf[r.idx : r.idx+m])
}
return RunesWidth(r.buf[r.idx+m : r.idx])
}
func (r *RuneBuffer) SetStyle(start, end int, style string) { func (r *RuneBuffer) SetStyle(start, end int, style string) {
idx := r.idx
if end < start { if end < start {
panic("end < start") panic("end < start")
} }
// goto start // goto start
move := start - idx move := start - r.idx
if move > 0 { if move > 0 {
r.w.Write([]byte(string(r.buf[r.idx : r.idx+move]))) r.w.Write([]byte(string(r.buf[r.idx : r.idx+move])))
} else { } else {
r.w.Write(bytes.Repeat([]byte("\b"), -move)) r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move)))
} }
r.w.Write([]byte("\033[" + style)) r.w.Write([]byte("\033[" + style))
r.w.Write([]byte(string(r.buf[start:end]))) r.w.Write([]byte(string(r.buf[start:end])))
r.w.Write([]byte("\033[0m")) r.w.Write([]byte("\033[0m"))
if move > 0 { // TODO: move back
r.w.Write(bytes.Repeat([]byte("\b"), -move+(end-start)))
} else if -move < end-start {
r.w.Write(bytes.Repeat([]byte("\b"), -move))
} else {
r.w.Write([]byte(string(r.buf[end:r.idx])))
}
} }
func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) { func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) {

View File

@ -121,6 +121,7 @@ func (o *opSearch) SearchRefresh(x int) {
if x < 0 { if x < 0 {
x = o.buf.idx x = o.buf.idx
} }
x = o.buf.CurrentWidth(x)
x += o.buf.PromptLen() x += o.buf.PromptLen()
x = x % getWidth() x = x % getWidth()

View File

@ -6,7 +6,7 @@ import (
"os" "os"
"syscall" "syscall"
"time" "time"
"unicode/utf8" "unicode"
"unsafe" "unsafe"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
@ -128,17 +128,6 @@ func LineCount(w int) int {
return r return r
} }
func RunesWidth(r []rune) (length int) {
for i := 0; i < len(r); i++ {
if utf8.RuneLen(r[i]) > 3 {
length += 2
} else {
length += 1
}
}
return
}
func RunesIndexBck(r, sub []rune) int { func RunesIndexBck(r, sub []rune) int {
for i := len(r) - len(sub); i >= 0; i-- { for i := len(r) - len(sub); i >= 0; i-- {
found := true found := true
@ -183,3 +172,59 @@ func IsWordBreak(i rune) bool {
} }
return true return true
} }
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 RuneIndex(r rune, rs []rune) int {
for i := 0; i < len(rs); i++ {
if rs[i] == r {
return i
}
}
return -1
}
func RunesColorFilter(r []rune) []rune {
newr := make([]rune, 0, len(r))
for pos := 0; pos < len(r); pos++ {
if r[pos] == '\033' && r[pos+1] == '[' {
idx := RuneIndex('m', r[pos+2:])
if idx == -1 {
continue
}
pos += idx + 2
continue
}
newr = append(newr, r[pos])
}
return newr
}
func RuneWidth(r rune) int {
if unicode.IsOneOf(zeroWidth, r) {
return 0
}
if unicode.IsOneOf(doubleWidth, r) {
return 2
}
return 1
}
func RunesWidth(r []rune) (length int) {
for i := 0; i < len(r); i++ {
length += RuneWidth(r[i])
}
return
}

22
utils_test.go Normal file
View File

@ -0,0 +1,22 @@
package readline
import "testing"
type Twidth struct {
r []rune
length int
}
func TestRuneWidth(t *testing.T) {
runes := []Twidth{
{[]rune("☭"), 1},
{[]rune("a"), 1},
{[]rune("你"), 2},
{RunesColorFilter([]rune("☭\033[13;1m你")), 3},
}
for _, r := range runes {
if w := RunesWidth(r.r); w != r.length {
t.Fatal("result not expect", r.r, r.length, w)
}
}
}