2023-10-25 23:41:18 +00:00
|
|
|
package readline
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2023-10-30 20:18:12 +00:00
|
|
|
"os"
|
2023-10-25 23:41:18 +00:00
|
|
|
|
|
|
|
"github.com/emirpasic/gods/lists/arraylist"
|
|
|
|
"golang.org/x/term"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Buffer struct {
|
|
|
|
Pos int
|
|
|
|
Buf *arraylist.List
|
|
|
|
Prompt *Prompt
|
|
|
|
LineWidth int
|
|
|
|
Width int
|
|
|
|
Height int
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewBuffer(prompt *Prompt) (*Buffer, error) {
|
2023-10-30 20:18:12 +00:00
|
|
|
fd := int(os.Stdout.Fd())
|
|
|
|
width, height, err := term.GetSize(fd)
|
2023-10-25 23:41:18 +00:00
|
|
|
if err != nil {
|
|
|
|
fmt.Println("Error getting size:", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-01-06 00:19:37 +00:00
|
|
|
lwidth := width - len(prompt.prompt())
|
2023-10-25 23:41:18 +00:00
|
|
|
|
|
|
|
b := &Buffer{
|
|
|
|
Pos: 0,
|
|
|
|
Buf: arraylist.New(),
|
|
|
|
Prompt: prompt,
|
|
|
|
Width: width,
|
|
|
|
Height: height,
|
|
|
|
LineWidth: lwidth,
|
|
|
|
}
|
|
|
|
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) MoveLeft() {
|
|
|
|
if b.Pos > 0 {
|
|
|
|
if b.Pos%b.LineWidth == 0 {
|
|
|
|
fmt.Printf(CursorUp + CursorBOL + cursorRightN(b.Width))
|
|
|
|
} else {
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(CursorLeft)
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
b.Pos -= 1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.Size() {
|
|
|
|
b.Pos += 1
|
|
|
|
if b.Pos%b.LineWidth == 0 {
|
2024-01-06 00:19:37 +00:00
|
|
|
fmt.Printf(CursorDown + CursorBOL + cursorRightN(len(b.Prompt.prompt())))
|
2023-10-25 23:41:18 +00:00
|
|
|
} else {
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(CursorRight)
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) MoveRightWord() {
|
|
|
|
if b.Pos < b.Size() {
|
|
|
|
for {
|
|
|
|
b.MoveRight()
|
|
|
|
v, _ := b.Buf.Get(b.Pos)
|
|
|
|
if v == ' ' {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
if b.Pos == b.Size() {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) MoveToStart() {
|
|
|
|
if b.Pos > 0 {
|
|
|
|
currLine := b.Pos / b.LineWidth
|
|
|
|
if currLine > 0 {
|
|
|
|
for cnt := 0; cnt < currLine; cnt++ {
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(CursorUp)
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
2024-01-06 00:19:37 +00:00
|
|
|
fmt.Printf(CursorBOL + cursorRightN(len(b.Prompt.prompt())))
|
2023-10-25 23:41:18 +00:00
|
|
|
b.Pos = 0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) MoveToEnd() {
|
|
|
|
if b.Pos < b.Size() {
|
|
|
|
currLine := b.Pos / b.LineWidth
|
|
|
|
totalLines := b.Size() / b.LineWidth
|
|
|
|
if currLine < totalLines {
|
|
|
|
for cnt := 0; cnt < totalLines-currLine; cnt++ {
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(CursorDown)
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
remainder := b.Size() % b.LineWidth
|
2024-01-06 00:19:37 +00:00
|
|
|
fmt.Printf(CursorBOL + cursorRightN(len(b.Prompt.prompt())+remainder))
|
2023-10-25 23:41:18 +00:00
|
|
|
} else {
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(cursorRightN(b.Size() - b.Pos))
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
b.Pos = b.Size()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) Size() int {
|
|
|
|
return b.Buf.Size()
|
|
|
|
}
|
|
|
|
|
|
|
|
func min(n, m int) int {
|
|
|
|
if n > m {
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) Add(r rune) {
|
|
|
|
if b.Pos == b.Buf.Size() {
|
|
|
|
fmt.Printf("%c", r)
|
|
|
|
b.Buf.Add(r)
|
|
|
|
b.Pos += 1
|
|
|
|
if b.Pos > 0 && b.Pos%b.LineWidth == 0 {
|
|
|
|
fmt.Printf("\n%s", b.Prompt.AltPrompt)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fmt.Printf("%c", r)
|
|
|
|
b.Buf.Insert(b.Pos, r)
|
|
|
|
b.Pos += 1
|
|
|
|
if b.Pos > 0 && b.Pos%b.LineWidth == 0 {
|
|
|
|
fmt.Printf("\n%s", b.Prompt.AltPrompt)
|
|
|
|
}
|
|
|
|
b.drawRemaining()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) drawRemaining() {
|
|
|
|
var place int
|
|
|
|
remainingText := b.StringN(b.Pos)
|
|
|
|
if b.Pos > 0 {
|
|
|
|
place = b.Pos % b.LineWidth
|
|
|
|
}
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(CursorHide)
|
2023-10-25 23:41:18 +00:00
|
|
|
|
|
|
|
// render the rest of the current line
|
|
|
|
currLine := remainingText[:min(b.LineWidth-place, len(remainingText))]
|
|
|
|
if len(currLine) > 0 {
|
|
|
|
fmt.Printf(ClearToEOL + currLine)
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(cursorLeftN(len(currLine)))
|
2023-10-25 23:41:18 +00:00
|
|
|
} else {
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(ClearToEOL)
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// render the other lines
|
|
|
|
if len(remainingText) > len(currLine) {
|
|
|
|
remaining := []rune(remainingText[len(currLine):])
|
|
|
|
var totalLines int
|
|
|
|
for i, c := range remaining {
|
|
|
|
if i%b.LineWidth == 0 {
|
|
|
|
fmt.Printf("\n%s", b.Prompt.AltPrompt)
|
|
|
|
totalLines += 1
|
|
|
|
}
|
|
|
|
fmt.Printf("%c", c)
|
|
|
|
}
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(ClearToEOL)
|
|
|
|
fmt.Print(cursorUpN(totalLines))
|
2023-10-25 23:41:18 +00:00
|
|
|
fmt.Printf(CursorBOL + cursorRightN(b.Width-len(currLine)))
|
|
|
|
}
|
|
|
|
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(CursorShow)
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) Remove() {
|
|
|
|
if b.Buf.Size() > 0 && b.Pos > 0 {
|
|
|
|
if b.Pos%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) + " " + CursorLeft)
|
|
|
|
} else {
|
|
|
|
fmt.Printf(CursorLeft + " " + CursorLeft)
|
|
|
|
}
|
|
|
|
|
|
|
|
var eraseExtraLine bool
|
|
|
|
if (b.Size()-1)%b.LineWidth == 0 {
|
|
|
|
eraseExtraLine = true
|
|
|
|
}
|
|
|
|
|
|
|
|
b.Pos -= 1
|
|
|
|
b.Buf.Remove(b.Pos)
|
|
|
|
|
|
|
|
if b.Pos < b.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.Size() - b.Pos) / b.LineWidth
|
|
|
|
fmt.Printf(cursorDownN(remainingLines+1) + CursorBOL + ClearToEOL)
|
|
|
|
place := b.Pos % b.LineWidth
|
2024-01-06 00:19:37 +00:00
|
|
|
fmt.Printf(cursorUpN(remainingLines+1) + cursorRightN(place+len(b.Prompt.prompt())))
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) Delete() {
|
|
|
|
if b.Size() > 0 && b.Pos < b.Size() {
|
|
|
|
b.Buf.Remove(b.Pos)
|
|
|
|
b.drawRemaining()
|
|
|
|
if b.Size()%b.LineWidth == 0 {
|
|
|
|
if b.Pos != b.Size() {
|
|
|
|
remainingLines := (b.Size() - b.Pos) / b.LineWidth
|
|
|
|
fmt.Printf(cursorDownN(remainingLines) + CursorBOL + ClearToEOL)
|
|
|
|
place := b.Pos % b.LineWidth
|
2024-01-06 00:19:37 +00:00
|
|
|
fmt.Printf(cursorUpN(remainingLines) + cursorRightN(place+len(b.Prompt.prompt())))
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) DeleteBefore() {
|
|
|
|
if b.Pos > 0 {
|
|
|
|
for cnt := b.Pos - 1; cnt >= 0; cnt-- {
|
|
|
|
b.Remove()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) DeleteRemaining() {
|
|
|
|
if b.Size() > 0 && b.Pos < b.Size() {
|
|
|
|
charsToDel := b.Size() - b.Pos
|
|
|
|
for cnt := 0; cnt < charsToDel; cnt++ {
|
|
|
|
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() {
|
2024-01-06 00:19:37 +00:00
|
|
|
fmt.Printf(ClearScreen + CursorReset + b.Prompt.prompt())
|
2023-10-25 23:41:18 +00:00
|
|
|
if b.IsEmpty() {
|
2024-01-06 00:19:37 +00:00
|
|
|
ph := b.Prompt.placeholder()
|
2023-10-25 23:41:18 +00:00
|
|
|
fmt.Printf(ColorGrey + ph + cursorLeftN(len(ph)) + ColorDefault)
|
|
|
|
} else {
|
|
|
|
currPos := b.Pos
|
|
|
|
b.Pos = 0
|
|
|
|
b.drawRemaining()
|
2024-01-06 00:19:37 +00:00
|
|
|
fmt.Printf(CursorReset + cursorRightN(len(b.Prompt.prompt())))
|
2023-10-25 23:41:18 +00:00
|
|
|
if currPos > 0 {
|
|
|
|
targetLine := currPos / b.LineWidth
|
|
|
|
if targetLine > 0 {
|
|
|
|
for cnt := 0; cnt < targetLine; cnt++ {
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(CursorDown)
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
remainder := currPos % b.LineWidth
|
|
|
|
if remainder > 0 {
|
2023-10-28 03:01:48 +00:00
|
|
|
fmt.Print(cursorRightN(remainder))
|
2023-10-25 23:41:18 +00:00
|
|
|
}
|
|
|
|
if currPos%b.LineWidth == 0 {
|
|
|
|
fmt.Printf(CursorBOL + b.Prompt.AltPrompt)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
b.Pos = currPos
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) IsEmpty() bool {
|
|
|
|
return b.Buf.Empty()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *Buffer) Replace(r []rune) {
|
|
|
|
b.Pos = 0
|
|
|
|
b.Buf.Clear()
|
2024-01-06 00:19:37 +00:00
|
|
|
fmt.Printf(ClearLine + CursorBOL + b.Prompt.prompt())
|
2023-10-25 23:41:18 +00:00
|
|
|
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.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)
|
|
|
|
}
|