123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- 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 is an arraylist of bools to keep track of whether a line has a space at the end
- 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 {
- //asserts that we retrieve a rune
- 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)
- }
- // this case occurs when a double-width rune crosses the line boundary
- } 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)
- // render the rest of the current line
- 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))
- }
- // render the other lines
- 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 {
- // if the user backspaces over the word boundary, do this magic to clear the line
- // and move to the end of the previous line
- 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()
- // this erases a line which is left over when backspacing in the middle of a line and there
- // are trailing characters which go over the line width boundary
- 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)
- }
|