|
package readline |
|
|
|
import ( |
|
"fmt" |
|
"os" |
|
|
|
"github.com/emirpasic/gods/lists/arraylist" |
|
"github.com/mattn/go-runewidth" |
|
"golang.org/x/term" |
|
) |
|
|
|
type Buffer struct { |
|
DisplayPos int |
|
Pos int |
|
Buf *arraylist.List |
|
|
|
LineHasSpace *arraylist.List |
|
Prompt *Prompt |
|
LineWidth int |
|
Width int |
|
Height int |
|
} |
|
|
|
func NewBuffer(prompt *Prompt) (*Buffer, error) { |
|
fd := int(os.Stdout.Fd()) |
|
width, height := 80, 24 |
|
if termWidth, termHeight, err := term.GetSize(fd); err == nil { |
|
width, height = termWidth, termHeight |
|
} |
|
|
|
lwidth := width - len(prompt.prompt()) |
|
|
|
b := &Buffer{ |
|
DisplayPos: 0, |
|
Pos: 0, |
|
Buf: arraylist.New(), |
|
LineHasSpace: arraylist.New(), |
|
Prompt: prompt, |
|
Width: width, |
|
Height: height, |
|
LineWidth: lwidth, |
|
} |
|
|
|
return b, nil |
|
} |
|
|
|
func (b *Buffer) GetLineSpacing(line int) bool { |
|
hasSpace, _ := b.LineHasSpace.Get(line) |
|
|
|
if hasSpace == nil { |
|
return false |
|
} |
|
|
|
return hasSpace.(bool) |
|
} |
|
|
|
func (b *Buffer) MoveLeft() { |
|
if b.Pos > 0 { |
|
|
|
if e, ok := b.Buf.Get(b.Pos - 1); ok { |
|
if r, ok := e.(rune); ok { |
|
rLength := runewidth.RuneWidth(r) |
|
|
|
if b.DisplayPos%b.LineWidth == 0 { |
|
fmt.Printf(CursorUp + CursorBOL + cursorRightN(b.Width)) |
|
if rLength == 2 { |
|
fmt.Print(CursorLeft) |
|
} |
|
|
|
line := b.DisplayPos/b.LineWidth - 1 |
|
hasSpace := b.GetLineSpacing(line) |
|
if hasSpace { |
|
b.DisplayPos -= 1 |
|
fmt.Print(CursorLeft) |
|
} |
|
} else { |
|
fmt.Print(cursorLeftN(rLength)) |
|
} |
|
|
|
b.Pos -= 1 |
|
b.DisplayPos -= rLength |
|
} |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) MoveLeftWord() { |
|
if b.Pos > 0 { |
|
var foundNonspace bool |
|
for { |
|
v, _ := b.Buf.Get(b.Pos - 1) |
|
if v == ' ' { |
|
if foundNonspace { |
|
break |
|
} |
|
} else { |
|
foundNonspace = true |
|
} |
|
b.MoveLeft() |
|
|
|
if b.Pos == 0 { |
|
break |
|
} |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) MoveRight() { |
|
if b.Pos < b.Buf.Size() { |
|
if e, ok := b.Buf.Get(b.Pos); ok { |
|
if r, ok := e.(rune); ok { |
|
rLength := runewidth.RuneWidth(r) |
|
b.Pos += 1 |
|
hasSpace := b.GetLineSpacing(b.DisplayPos / b.LineWidth) |
|
b.DisplayPos += rLength |
|
|
|
if b.DisplayPos%b.LineWidth == 0 { |
|
fmt.Printf(CursorDown + CursorBOL + cursorRightN(len(b.Prompt.prompt()))) |
|
} else if (b.DisplayPos-rLength)%b.LineWidth == b.LineWidth-1 && hasSpace { |
|
fmt.Printf(CursorDown + CursorBOL + cursorRightN(len(b.Prompt.prompt())+rLength)) |
|
b.DisplayPos += 1 |
|
} else if b.LineHasSpace.Size() > 0 && b.DisplayPos%b.LineWidth == b.LineWidth-1 && hasSpace { |
|
fmt.Printf(CursorDown + CursorBOL + cursorRightN(len(b.Prompt.prompt()))) |
|
b.DisplayPos += 1 |
|
} else { |
|
fmt.Print(cursorRightN(rLength)) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) MoveRightWord() { |
|
if b.Pos < b.Buf.Size() { |
|
for { |
|
b.MoveRight() |
|
v, _ := b.Buf.Get(b.Pos) |
|
if v == ' ' { |
|
break |
|
} |
|
|
|
if b.Pos == b.Buf.Size() { |
|
break |
|
} |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) MoveToStart() { |
|
if b.Pos > 0 { |
|
currLine := b.DisplayPos / b.LineWidth |
|
if currLine > 0 { |
|
for range currLine { |
|
fmt.Print(CursorUp) |
|
} |
|
} |
|
fmt.Printf(CursorBOL + cursorRightN(len(b.Prompt.prompt()))) |
|
b.Pos = 0 |
|
b.DisplayPos = 0 |
|
} |
|
} |
|
|
|
func (b *Buffer) MoveToEnd() { |
|
if b.Pos < b.Buf.Size() { |
|
currLine := b.DisplayPos / b.LineWidth |
|
totalLines := b.DisplaySize() / b.LineWidth |
|
if currLine < totalLines { |
|
for range totalLines - currLine { |
|
fmt.Print(CursorDown) |
|
} |
|
remainder := b.DisplaySize() % b.LineWidth |
|
fmt.Printf(CursorBOL + cursorRightN(len(b.Prompt.prompt())+remainder)) |
|
} else { |
|
fmt.Print(cursorRightN(b.DisplaySize() - b.DisplayPos)) |
|
} |
|
|
|
b.Pos = b.Buf.Size() |
|
b.DisplayPos = b.DisplaySize() |
|
} |
|
} |
|
|
|
func (b *Buffer) DisplaySize() int { |
|
sum := 0 |
|
for i := range b.Buf.Size() { |
|
if e, ok := b.Buf.Get(i); ok { |
|
if r, ok := e.(rune); ok { |
|
sum += runewidth.RuneWidth(r) |
|
} |
|
} |
|
} |
|
|
|
return sum |
|
} |
|
|
|
func (b *Buffer) Add(r rune) { |
|
if b.Pos == b.Buf.Size() { |
|
b.AddChar(r, false) |
|
} else { |
|
b.AddChar(r, true) |
|
} |
|
} |
|
|
|
func (b *Buffer) AddChar(r rune, insert bool) { |
|
rLength := runewidth.RuneWidth(r) |
|
b.DisplayPos += rLength |
|
|
|
if b.Pos > 0 { |
|
if b.DisplayPos%b.LineWidth == 0 { |
|
fmt.Printf("%c", r) |
|
fmt.Printf("\n%s", b.Prompt.AltPrompt) |
|
|
|
if insert { |
|
b.LineHasSpace.Set(b.DisplayPos/b.LineWidth-1, false) |
|
} else { |
|
b.LineHasSpace.Add(false) |
|
} |
|
|
|
|
|
} else if b.DisplayPos%b.LineWidth < (b.DisplayPos-rLength)%b.LineWidth { |
|
if insert { |
|
fmt.Print(ClearToEOL) |
|
} |
|
fmt.Printf("\n%s", b.Prompt.AltPrompt) |
|
b.DisplayPos += 1 |
|
fmt.Printf("%c", r) |
|
|
|
if insert { |
|
b.LineHasSpace.Set(b.DisplayPos/b.LineWidth-1, true) |
|
} else { |
|
b.LineHasSpace.Add(true) |
|
} |
|
} else { |
|
fmt.Printf("%c", r) |
|
} |
|
} else { |
|
fmt.Printf("%c", r) |
|
} |
|
|
|
if insert { |
|
b.Buf.Insert(b.Pos, r) |
|
} else { |
|
b.Buf.Add(r) |
|
} |
|
|
|
b.Pos += 1 |
|
|
|
if insert { |
|
b.drawRemaining() |
|
} |
|
} |
|
|
|
func (b *Buffer) countRemainingLineWidth(place int) int { |
|
var sum int |
|
counter := -1 |
|
var prevLen int |
|
|
|
for place <= b.LineWidth { |
|
counter += 1 |
|
sum += prevLen |
|
if e, ok := b.Buf.Get(b.Pos + counter); ok { |
|
if r, ok := e.(rune); ok { |
|
place += runewidth.RuneWidth(r) |
|
prevLen = len(string(r)) |
|
} |
|
} else { |
|
break |
|
} |
|
} |
|
|
|
return sum |
|
} |
|
|
|
func (b *Buffer) drawRemaining() { |
|
var place int |
|
remainingText := b.StringN(b.Pos) |
|
if b.Pos > 0 { |
|
place = b.DisplayPos % b.LineWidth |
|
} |
|
fmt.Print(CursorHide) |
|
|
|
|
|
currLineLength := b.countRemainingLineWidth(place) |
|
|
|
currLine := remainingText[:min(currLineLength, len(remainingText))] |
|
currLineSpace := runewidth.StringWidth(currLine) |
|
remLength := runewidth.StringWidth(remainingText) |
|
|
|
if len(currLine) > 0 { |
|
fmt.Printf(ClearToEOL + currLine) |
|
fmt.Print(cursorLeftN(currLineSpace)) |
|
} else { |
|
fmt.Print(ClearToEOL) |
|
} |
|
|
|
if currLineSpace != b.LineWidth-place && currLineSpace != remLength { |
|
b.LineHasSpace.Set(b.DisplayPos/b.LineWidth, true) |
|
} else if currLineSpace != b.LineWidth-place { |
|
b.LineHasSpace.Remove(b.DisplayPos / b.LineWidth) |
|
} else { |
|
b.LineHasSpace.Set(b.DisplayPos/b.LineWidth, false) |
|
} |
|
|
|
if (b.DisplayPos+currLineSpace)%b.LineWidth == 0 && currLine == remainingText { |
|
fmt.Print(cursorRightN(currLineSpace)) |
|
fmt.Printf("\n%s", b.Prompt.AltPrompt) |
|
fmt.Printf(CursorUp + CursorBOL + cursorRightN(b.Width-currLineSpace)) |
|
} |
|
|
|
|
|
if remLength > currLineSpace { |
|
remaining := (remainingText[len(currLine):]) |
|
var totalLines int |
|
var displayLength int |
|
var lineLength int = currLineSpace |
|
|
|
for _, c := range remaining { |
|
if displayLength == 0 || (displayLength+runewidth.RuneWidth(c))%b.LineWidth < displayLength%b.LineWidth { |
|
fmt.Printf("\n%s", b.Prompt.AltPrompt) |
|
totalLines += 1 |
|
|
|
if displayLength != 0 { |
|
if lineLength == b.LineWidth { |
|
b.LineHasSpace.Set(b.DisplayPos/b.LineWidth+totalLines-1, false) |
|
} else { |
|
b.LineHasSpace.Set(b.DisplayPos/b.LineWidth+totalLines-1, true) |
|
} |
|
} |
|
|
|
lineLength = 0 |
|
} |
|
|
|
displayLength += runewidth.RuneWidth(c) |
|
lineLength += runewidth.RuneWidth(c) |
|
fmt.Printf("%c", c) |
|
} |
|
fmt.Print(ClearToEOL) |
|
fmt.Print(cursorUpN(totalLines)) |
|
fmt.Printf(CursorBOL + cursorRightN(b.Width-currLineSpace)) |
|
|
|
hasSpace := b.GetLineSpacing(b.DisplayPos / b.LineWidth) |
|
|
|
if hasSpace && b.DisplayPos%b.LineWidth != b.LineWidth-1 { |
|
fmt.Print(CursorLeft) |
|
} |
|
} |
|
|
|
fmt.Print(CursorShow) |
|
} |
|
|
|
func (b *Buffer) Remove() { |
|
if b.Buf.Size() > 0 && b.Pos > 0 { |
|
if e, ok := b.Buf.Get(b.Pos - 1); ok { |
|
if r, ok := e.(rune); ok { |
|
rLength := runewidth.RuneWidth(r) |
|
hasSpace := b.GetLineSpacing(b.DisplayPos/b.LineWidth - 1) |
|
|
|
if b.DisplayPos%b.LineWidth == 0 { |
|
|
|
|
|
fmt.Printf(CursorBOL + ClearToEOL) |
|
fmt.Printf(CursorUp + CursorBOL + cursorRightN(b.Width)) |
|
|
|
if b.DisplaySize()%b.LineWidth < (b.DisplaySize()-rLength)%b.LineWidth { |
|
b.LineHasSpace.Remove(b.DisplayPos/b.LineWidth - 1) |
|
} |
|
|
|
if hasSpace { |
|
b.DisplayPos -= 1 |
|
fmt.Print(CursorLeft) |
|
} |
|
|
|
if rLength == 2 { |
|
fmt.Print(CursorLeft + " " + cursorLeftN(2)) |
|
} else { |
|
fmt.Print(" " + CursorLeft) |
|
} |
|
} else if (b.DisplayPos-rLength)%b.LineWidth == 0 && hasSpace { |
|
fmt.Printf(CursorBOL + ClearToEOL) |
|
fmt.Printf(CursorUp + CursorBOL + cursorRightN(b.Width)) |
|
|
|
if b.Pos == b.Buf.Size() { |
|
b.LineHasSpace.Remove(b.DisplayPos/b.LineWidth - 1) |
|
} |
|
b.DisplayPos -= 1 |
|
} else { |
|
fmt.Print(cursorLeftN(rLength)) |
|
for range rLength { |
|
fmt.Print(" ") |
|
} |
|
fmt.Print(cursorLeftN(rLength)) |
|
} |
|
|
|
var eraseExtraLine bool |
|
if (b.DisplaySize()-1)%b.LineWidth == 0 || (rLength == 2 && ((b.DisplaySize()-2)%b.LineWidth == 0)) || b.DisplaySize()%b.LineWidth == 0 { |
|
eraseExtraLine = true |
|
} |
|
|
|
b.Pos -= 1 |
|
b.DisplayPos -= rLength |
|
b.Buf.Remove(b.Pos) |
|
|
|
if b.Pos < b.Buf.Size() { |
|
b.drawRemaining() |
|
|
|
|
|
if eraseExtraLine { |
|
remainingLines := (b.DisplaySize() - b.DisplayPos) / b.LineWidth |
|
fmt.Printf(cursorDownN(remainingLines+1) + CursorBOL + ClearToEOL) |
|
place := b.DisplayPos % b.LineWidth |
|
fmt.Printf(cursorUpN(remainingLines+1) + cursorRightN(place+len(b.Prompt.prompt()))) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) Delete() { |
|
if b.Buf.Size() > 0 && b.Pos < b.Buf.Size() { |
|
b.Buf.Remove(b.Pos) |
|
b.drawRemaining() |
|
if b.DisplaySize()%b.LineWidth == 0 { |
|
if b.DisplayPos != b.DisplaySize() { |
|
remainingLines := (b.DisplaySize() - b.DisplayPos) / b.LineWidth |
|
fmt.Printf(cursorDownN(remainingLines) + CursorBOL + ClearToEOL) |
|
place := b.DisplayPos % b.LineWidth |
|
fmt.Printf(cursorUpN(remainingLines) + cursorRightN(place+len(b.Prompt.prompt()))) |
|
} |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) DeleteBefore() { |
|
if b.Pos > 0 { |
|
for cnt := b.Pos - 1; cnt >= 0; cnt-- { |
|
b.Remove() |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) DeleteRemaining() { |
|
if b.DisplaySize() > 0 && b.Pos < b.DisplaySize() { |
|
charsToDel := b.Buf.Size() - b.Pos |
|
for range charsToDel { |
|
b.Delete() |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) DeleteWord() { |
|
if b.Buf.Size() > 0 && b.Pos > 0 { |
|
var foundNonspace bool |
|
for { |
|
v, _ := b.Buf.Get(b.Pos - 1) |
|
if v == ' ' { |
|
if !foundNonspace { |
|
b.Remove() |
|
} else { |
|
break |
|
} |
|
} else { |
|
foundNonspace = true |
|
b.Remove() |
|
} |
|
|
|
if b.Pos == 0 { |
|
break |
|
} |
|
} |
|
} |
|
} |
|
|
|
func (b *Buffer) ClearScreen() { |
|
fmt.Printf(ClearScreen + CursorReset + b.Prompt.prompt()) |
|
if b.IsEmpty() { |
|
ph := b.Prompt.placeholder() |
|
fmt.Printf(ColorGrey + ph + cursorLeftN(len(ph)) + ColorDefault) |
|
} else { |
|
currPos := b.DisplayPos |
|
currIndex := b.Pos |
|
b.Pos = 0 |
|
b.DisplayPos = 0 |
|
b.drawRemaining() |
|
fmt.Printf(CursorReset + cursorRightN(len(b.Prompt.prompt()))) |
|
if currPos > 0 { |
|
targetLine := currPos / b.LineWidth |
|
if targetLine > 0 { |
|
for range targetLine { |
|
fmt.Print(CursorDown) |
|
} |
|
} |
|
remainder := currPos % b.LineWidth |
|
if remainder > 0 { |
|
fmt.Print(cursorRightN(remainder)) |
|
} |
|
if currPos%b.LineWidth == 0 { |
|
fmt.Printf(CursorBOL + b.Prompt.AltPrompt) |
|
} |
|
} |
|
b.Pos = currIndex |
|
b.DisplayPos = currPos |
|
} |
|
} |
|
|
|
func (b *Buffer) IsEmpty() bool { |
|
return b.Buf.Empty() |
|
} |
|
|
|
func (b *Buffer) Replace(r []rune) { |
|
b.DisplayPos = 0 |
|
b.Pos = 0 |
|
lineNums := b.DisplaySize() / b.LineWidth |
|
|
|
b.Buf.Clear() |
|
|
|
fmt.Printf(CursorBOL + ClearToEOL) |
|
|
|
for range lineNums { |
|
fmt.Print(CursorUp + CursorBOL + ClearToEOL) |
|
} |
|
|
|
fmt.Printf(CursorBOL + b.Prompt.prompt()) |
|
|
|
for _, c := range r { |
|
b.Add(c) |
|
} |
|
} |
|
|
|
func (b *Buffer) String() string { |
|
return b.StringN(0) |
|
} |
|
|
|
func (b *Buffer) StringN(n int) string { |
|
return b.StringNM(n, 0) |
|
} |
|
|
|
func (b *Buffer) StringNM(n, m int) string { |
|
var s string |
|
if m == 0 { |
|
m = b.Buf.Size() |
|
} |
|
for cnt := n; cnt < m; cnt++ { |
|
c, _ := b.Buf.Get(cnt) |
|
s += string(c.(rune)) |
|
} |
|
return s |
|
} |
|
|
|
func cursorLeftN(n int) string { |
|
return fmt.Sprintf(CursorLeftN, n) |
|
} |
|
|
|
func cursorRightN(n int) string { |
|
return fmt.Sprintf(CursorRightN, n) |
|
} |
|
|
|
func cursorUpN(n int) string { |
|
return fmt.Sprintf(CursorUpN, n) |
|
} |
|
|
|
func cursorDownN(n int) string { |
|
return fmt.Sprintf(CursorDownN, n) |
|
} |
|
|