Browse Source

chore: clean up readline package

Michael Yang 4 months ago
parent
commit
a1d90e68d0
3 changed files with 194 additions and 257 deletions
  1. 145 166
      readline/buffer.go
  2. 47 77
      readline/history.go
  3. 2 14
      readline/readline.go

+ 145 - 166
readline/buffer.go

@@ -10,15 +10,15 @@ import (
 )
 
 type Buffer struct {
-	DisplayPos int
-	Pos        int
-	Buf        *arraylist.List[rune]
-	// LineHasSpace is an arraylist of bools to keep track of whether a line has a space at the end
-	LineHasSpace *arraylist.List[bool]
-	Prompt       *Prompt
-	LineWidth    int
-	Width        int
-	Height       int
+	Prompt    *Prompt
+	LineWidth int
+	Width     int
+	Height    int
+
+	line       *arraylist.List[rune]
+	spaceMask  *arraylist.List[bool]
+	pos        int
+	displayPos int
 }
 
 func NewBuffer(prompt *Prompt) (*Buffer, error) {
@@ -30,130 +30,113 @@ func NewBuffer(prompt *Prompt) (*Buffer, error) {
 
 	lwidth := width - len(prompt.prompt())
 
-	b := &Buffer{
-		DisplayPos:   0,
-		Pos:          0,
-		Buf:          arraylist.New[rune](),
-		LineHasSpace: arraylist.New[bool](),
-		Prompt:       prompt,
-		Width:        width,
-		Height:       height,
-		LineWidth:    lwidth,
-	}
-
-	return b, nil
+	return &Buffer{
+		displayPos: 0,
+		pos:        0,
+		line:       arraylist.New[rune](),
+		spaceMask:  arraylist.New[bool](),
+		Prompt:     prompt,
+		Width:      width,
+		Height:     height,
+		LineWidth:  lwidth,
+	}, nil
 }
 
 func (b *Buffer) GetLineSpacing(line int) bool {
-	hasSpace, _ := b.LineHasSpace.Get(line)
+	hasSpace, _ := b.spaceMask.Get(line)
 	return hasSpace
 }
 
 func (b *Buffer) MoveLeft() {
-	if b.Pos > 0 {
-		// asserts that we retrieve a rune
-		if r, ok := b.Buf.Get(b.Pos - 1); ok {
-			rLength := runewidth.RuneWidth(r)
-
-			if b.DisplayPos%b.LineWidth == 0 {
-				fmt.Print(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))
+	if b.pos > 0 {
+		r, _ := b.line.Get(b.pos - 1)
+		rLength := runewidth.RuneWidth(r)
+
+		if b.displayPos%b.LineWidth == 0 {
+			fmt.Print(CursorUp + CursorBOL + CursorRightN(b.Width))
+			if rLength == 2 {
+				fmt.Print(CursorLeft)
 			}
 
-			b.Pos -= 1
-			b.DisplayPos -= rLength
+			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 {
+	var foundNonspace bool
+	for b.pos > 0 {
+		v, _ := b.line.Get(b.pos - 1)
+		if v == ' ' {
+			if foundNonspace {
 				break
 			}
+		} else {
+			foundNonspace = true
 		}
+		b.MoveLeft()
 	}
 }
 
 func (b *Buffer) MoveRight() {
-	if b.Pos < b.Buf.Size() {
-		if r, ok := b.Buf.Get(b.Pos); 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.Print(CursorDown + CursorBOL + CursorRightN(len(b.Prompt.prompt())))
-			} else if (b.DisplayPos-rLength)%b.LineWidth == b.LineWidth-1 && hasSpace {
-				fmt.Print(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.Print(CursorDown + CursorBOL + CursorRightN(len(b.Prompt.prompt())))
-				b.DisplayPos += 1
-			} else {
-				fmt.Print(CursorRightN(rLength))
-			}
+	if b.pos < b.line.Size() {
+		r, _ := b.line.Get(b.pos)
+		rLength := runewidth.RuneWidth(r)
+		b.pos += 1
+		hasSpace := b.GetLineSpacing(b.displayPos / b.LineWidth)
+		b.displayPos += rLength
+
+		if b.displayPos%b.LineWidth == 0 {
+			fmt.Print(CursorDown + CursorBOL + CursorRightN(len(b.Prompt.prompt())))
+		} else if (b.displayPos-rLength)%b.LineWidth == b.LineWidth-1 && hasSpace {
+			fmt.Print(CursorDown + CursorBOL + CursorRightN(len(b.Prompt.prompt())+rLength))
+			b.displayPos += 1
+		} else if b.spaceMask.Size() > 0 && b.displayPos%b.LineWidth == b.LineWidth-1 && hasSpace {
+			fmt.Print(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
-			}
+	for b.pos < b.line.Size() {
+		b.MoveRight()
+		v, _ := b.line.Get(b.pos)
+		if v == ' ' {
+			break
 		}
 	}
 }
 
 func (b *Buffer) MoveToStart() {
-	if b.Pos > 0 {
-		currLine := b.DisplayPos / b.LineWidth
+	if b.pos > 0 {
+		currLine := b.displayPos / b.LineWidth
 		if currLine > 0 {
 			for range currLine {
 				fmt.Print(CursorUp)
 			}
 		}
 		fmt.Print(CursorBOL + CursorRightN(len(b.Prompt.prompt())))
-		b.Pos = 0
-		b.DisplayPos = 0
+		b.pos = 0
+		b.displayPos = 0
 	}
 }
 
 func (b *Buffer) MoveToEnd() {
-	if b.Pos < b.Buf.Size() {
-		currLine := b.DisplayPos / b.LineWidth
+	if b.pos < b.line.Size() {
+		currLine := b.displayPos / b.LineWidth
 		totalLines := b.DisplaySize() / b.LineWidth
 		if currLine < totalLines {
 			for range totalLines - currLine {
@@ -162,18 +145,18 @@ func (b *Buffer) MoveToEnd() {
 			remainder := b.DisplaySize() % b.LineWidth
 			fmt.Print(CursorBOL + CursorRightN(len(b.Prompt.prompt())+remainder))
 		} else {
-			fmt.Print(CursorRightN(b.DisplaySize() - b.DisplayPos))
+			fmt.Print(CursorRightN(b.DisplaySize() - b.displayPos))
 		}
 
-		b.Pos = b.Buf.Size()
-		b.DisplayPos = b.DisplaySize()
+		b.pos = b.line.Size()
+		b.displayPos = b.DisplaySize()
 	}
 }
 
 func (b *Buffer) DisplaySize() int {
 	sum := 0
-	for i := range b.Buf.Size() {
-		if r, ok := b.Buf.Get(i); ok {
+	for i := range b.line.Size() {
+		if r, ok := b.line.Get(i); ok {
 			sum += runewidth.RuneWidth(r)
 		}
 	}
@@ -182,7 +165,7 @@ func (b *Buffer) DisplaySize() int {
 }
 
 func (b *Buffer) Add(r rune) {
-	if b.Pos == b.Buf.Size() {
+	if b.pos == b.line.Size() {
 		b.AddChar(r, false)
 	} else {
 		b.AddChar(r, true)
@@ -191,32 +174,32 @@ func (b *Buffer) Add(r rune) {
 
 func (b *Buffer) AddChar(r rune, insert bool) {
 	rLength := runewidth.RuneWidth(r)
-	b.DisplayPos += rLength
+	b.displayPos += rLength
 
-	if b.Pos > 0 {
-		if b.DisplayPos%b.LineWidth == 0 {
+	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)
+				b.spaceMask.Set(b.displayPos/b.LineWidth-1, false)
 			} else {
-				b.LineHasSpace.Add(false)
+				b.spaceMask.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 {
+		} 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
+			b.displayPos += 1
 			fmt.Printf("%c", r)
 
 			if insert {
-				b.LineHasSpace.Set(b.DisplayPos/b.LineWidth-1, true)
+				b.spaceMask.Set(b.displayPos/b.LineWidth-1, true)
 			} else {
-				b.LineHasSpace.Add(true)
+				b.spaceMask.Add(true)
 			}
 		} else {
 			fmt.Printf("%c", r)
@@ -226,12 +209,12 @@ func (b *Buffer) AddChar(r rune, insert bool) {
 	}
 
 	if insert {
-		b.Buf.Insert(b.Pos, r)
+		b.line.Insert(b.pos, r)
 	} else {
-		b.Buf.Add(r)
+		b.line.Add(r)
 	}
 
-	b.Pos += 1
+	b.pos += 1
 
 	if insert {
 		b.drawRemaining()
@@ -246,7 +229,7 @@ func (b *Buffer) countRemainingLineWidth(place int) int {
 	for place <= b.LineWidth {
 		counter += 1
 		sum += prevLen
-		if r, ok := b.Buf.Get(b.Pos + counter); ok {
+		if r, ok := b.line.Get(b.pos + counter); ok {
 			place += runewidth.RuneWidth(r)
 			prevLen = len(string(r))
 		} else {
@@ -259,9 +242,9 @@ func (b *Buffer) countRemainingLineWidth(place int) int {
 
 func (b *Buffer) drawRemaining() {
 	var place int
-	remainingText := b.StringN(b.Pos)
-	if b.Pos > 0 {
-		place = b.DisplayPos % b.LineWidth
+	remainingText := b.StringN(b.pos)
+	if b.pos > 0 {
+		place = b.displayPos % b.LineWidth
 	}
 	fmt.Print(CursorHide)
 
@@ -279,14 +262,14 @@ func (b *Buffer) drawRemaining() {
 	}
 
 	if currLineSpace != b.LineWidth-place && currLineSpace != remLength {
-		b.LineHasSpace.Set(b.DisplayPos/b.LineWidth, true)
+		b.spaceMask.Set(b.displayPos/b.LineWidth, true)
 	} else if currLineSpace != b.LineWidth-place {
-		b.LineHasSpace.Remove(b.DisplayPos / b.LineWidth)
+		b.spaceMask.Remove(b.displayPos / b.LineWidth)
 	} else {
-		b.LineHasSpace.Set(b.DisplayPos/b.LineWidth, false)
+		b.spaceMask.Set(b.displayPos/b.LineWidth, false)
 	}
 
-	if (b.DisplayPos+currLineSpace)%b.LineWidth == 0 && currLine == remainingText {
+	if (b.displayPos+currLineSpace)%b.LineWidth == 0 && currLine == remainingText {
 		fmt.Print(CursorRightN(currLineSpace))
 		fmt.Printf("\n%s", b.Prompt.AltPrompt)
 		fmt.Print(CursorUp + CursorBOL + CursorRightN(b.Width-currLineSpace))
@@ -306,9 +289,9 @@ func (b *Buffer) drawRemaining() {
 
 				if displayLength != 0 {
 					if lineLength == b.LineWidth {
-						b.LineHasSpace.Set(b.DisplayPos/b.LineWidth+totalLines-1, false)
+						b.spaceMask.Set(b.displayPos/b.LineWidth+totalLines-1, false)
 					} else {
-						b.LineHasSpace.Set(b.DisplayPos/b.LineWidth+totalLines-1, true)
+						b.spaceMask.Set(b.displayPos/b.LineWidth+totalLines-1, true)
 					}
 				}
 
@@ -321,9 +304,9 @@ func (b *Buffer) drawRemaining() {
 		}
 		fmt.Print(ClearToEOL + CursorUpN(totalLines) + CursorBOL + CursorRightN(b.Width-currLineSpace))
 
-		hasSpace := b.GetLineSpacing(b.DisplayPos / b.LineWidth)
+		hasSpace := b.GetLineSpacing(b.displayPos / b.LineWidth)
 
-		if hasSpace && b.DisplayPos%b.LineWidth != b.LineWidth-1 {
+		if hasSpace && b.displayPos%b.LineWidth != b.LineWidth-1 {
 			fmt.Print(CursorLeft)
 		}
 	}
@@ -332,22 +315,22 @@ func (b *Buffer) drawRemaining() {
 }
 
 func (b *Buffer) Remove() {
-	if b.Buf.Size() > 0 && b.Pos > 0 {
-		if r, ok := b.Buf.Get(b.Pos - 1); ok {
+	if b.line.Size() > 0 && b.pos > 0 {
+		if r, ok := b.line.Get(b.pos - 1); ok {
 			rLength := runewidth.RuneWidth(r)
-			hasSpace := b.GetLineSpacing(b.DisplayPos/b.LineWidth - 1)
+			hasSpace := b.GetLineSpacing(b.displayPos/b.LineWidth - 1)
 
-			if b.DisplayPos%b.LineWidth == 0 {
+			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.Print(CursorBOL + ClearToEOL + CursorUp + CursorBOL + CursorRightN(b.Width))
 
 				if b.DisplaySize()%b.LineWidth < (b.DisplaySize()-rLength)%b.LineWidth {
-					b.LineHasSpace.Remove(b.DisplayPos/b.LineWidth - 1)
+					b.spaceMask.Remove(b.displayPos/b.LineWidth - 1)
 				}
 
 				if hasSpace {
-					b.DisplayPos -= 1
+					b.displayPos -= 1
 					fmt.Print(CursorLeft)
 				}
 
@@ -356,13 +339,13 @@ func (b *Buffer) Remove() {
 				} else {
 					fmt.Print(" " + CursorLeft)
 				}
-			} else if (b.DisplayPos-rLength)%b.LineWidth == 0 && hasSpace {
+			} else if (b.displayPos-rLength)%b.LineWidth == 0 && hasSpace {
 				fmt.Print(CursorBOL + ClearToEOL + CursorUp + CursorBOL + CursorRightN(b.Width))
 
-				if b.Pos == b.Buf.Size() {
-					b.LineHasSpace.Remove(b.DisplayPos/b.LineWidth - 1)
+				if b.pos == b.line.Size() {
+					b.spaceMask.Remove(b.displayPos/b.LineWidth - 1)
 				}
-				b.DisplayPos -= 1
+				b.displayPos -= 1
 			} else {
 				fmt.Print(CursorLeftN(rLength))
 				for range rLength {
@@ -376,18 +359,18 @@ func (b *Buffer) Remove() {
 				eraseExtraLine = true
 			}
 
-			b.Pos -= 1
-			b.DisplayPos -= rLength
-			b.Buf.Remove(b.Pos)
+			b.pos -= 1
+			b.displayPos -= rLength
+			b.line.Remove(b.pos)
 
-			if b.Pos < b.Buf.Size() {
+			if b.pos < b.line.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
+					remainingLines := (b.DisplaySize() - b.displayPos) / b.LineWidth
 					fmt.Print(CursorDownN(remainingLines+1) + CursorBOL + ClearToEOL)
-					place := b.DisplayPos % b.LineWidth
+					place := b.displayPos % b.LineWidth
 					fmt.Print(CursorUpN(remainingLines+1) + CursorRightN(place+len(b.Prompt.prompt())))
 				}
 			}
@@ -396,14 +379,14 @@ func (b *Buffer) Remove() {
 }
 
 func (b *Buffer) Delete() {
-	if b.Buf.Size() > 0 && b.Pos < b.Buf.Size() {
-		b.Buf.Remove(b.Pos)
+	if b.line.Size() > 0 && b.pos < b.line.Size() {
+		b.line.Remove(b.pos)
 		b.drawRemaining()
 		if b.DisplaySize()%b.LineWidth == 0 {
-			if b.DisplayPos != b.DisplaySize() {
-				remainingLines := (b.DisplaySize() - b.DisplayPos) / b.LineWidth
+			if b.displayPos != b.DisplaySize() {
+				remainingLines := (b.DisplaySize() - b.displayPos) / b.LineWidth
 				fmt.Print(CursorDownN(remainingLines) + CursorBOL + ClearToEOL)
-				place := b.DisplayPos % b.LineWidth
+				place := b.displayPos % b.LineWidth
 				fmt.Print(CursorUpN(remainingLines) + CursorRightN(place+len(b.Prompt.prompt())))
 			}
 		}
@@ -411,16 +394,16 @@ func (b *Buffer) Delete() {
 }
 
 func (b *Buffer) DeleteBefore() {
-	if b.Pos > 0 {
-		for cnt := b.Pos - 1; cnt >= 0; cnt-- {
+	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
+	if b.DisplaySize() > 0 && b.pos < b.DisplaySize() {
+		charsToDel := b.line.Size() - b.pos
 		for range charsToDel {
 			b.Delete()
 		}
@@ -428,10 +411,10 @@ func (b *Buffer) DeleteRemaining() {
 }
 
 func (b *Buffer) DeleteWord() {
-	if b.Buf.Size() > 0 && b.Pos > 0 {
+	if b.line.Size() > 0 {
 		var foundNonspace bool
-		for {
-			v, _ := b.Buf.Get(b.Pos - 1)
+		for b.pos > 0 {
+			v, _ := b.line.Get(b.pos - 1)
 			if v == ' ' {
 				if !foundNonspace {
 					b.Remove()
@@ -442,10 +425,6 @@ func (b *Buffer) DeleteWord() {
 				foundNonspace = true
 				b.Remove()
 			}
-
-			if b.Pos == 0 {
-				break
-			}
 		}
 	}
 }
@@ -456,10 +435,10 @@ func (b *Buffer) ClearScreen() {
 		ph := b.Prompt.placeholder()
 		fmt.Print(ColorGrey + ph + CursorLeftN(len(ph)) + ColorDefault)
 	} else {
-		currPos := b.DisplayPos
-		currIndex := b.Pos
-		b.Pos = 0
-		b.DisplayPos = 0
+		currPos := b.displayPos
+		currIndex := b.pos
+		b.pos = 0
+		b.displayPos = 0
 		b.drawRemaining()
 		fmt.Print(CursorReset + CursorRightN(len(b.Prompt.prompt())))
 		if currPos > 0 {
@@ -477,21 +456,21 @@ func (b *Buffer) ClearScreen() {
 				fmt.Print(CursorBOL + b.Prompt.AltPrompt)
 			}
 		}
-		b.Pos = currIndex
-		b.DisplayPos = currPos
+		b.pos = currIndex
+		b.displayPos = currPos
 	}
 }
 
 func (b *Buffer) IsEmpty() bool {
-	return b.Buf.Empty()
+	return b.line.Empty()
 }
 
 func (b *Buffer) Replace(r []rune) {
-	b.DisplayPos = 0
-	b.Pos = 0
+	b.displayPos = 0
+	b.pos = 0
 	lineNums := b.DisplaySize() / b.LineWidth
 
-	b.Buf.Clear()
+	b.line.Clear()
 
 	fmt.Print(CursorBOL + ClearToEOL)
 
@@ -517,10 +496,10 @@ func (b *Buffer) StringN(n int) string {
 func (b *Buffer) StringNM(n, m int) string {
 	var s string
 	if m == 0 {
-		m = b.Buf.Size()
+		m = b.line.Size()
 	}
 	for cnt := n; cnt < m; cnt++ {
-		c, _ := b.Buf.Get(cnt)
+		c, _ := b.line.Get(cnt)
 		s += string(c)
 	}
 	return s

+ 47 - 77
readline/history.go

@@ -2,9 +2,7 @@ package readline
 
 import (
 	"bufio"
-	"errors"
 	"fmt"
-	"io"
 	"os"
 	"path/filepath"
 	"strings"
@@ -13,115 +11,91 @@ import (
 )
 
 type History struct {
-	Buf      *arraylist.List[string]
-	Autosave bool
-	Pos      int
-	Limit    int
-	Filename string
-	Enabled  bool
+	Enabled bool
+
+	lines    *arraylist.List[string]
+	limit    int
+	pos      int
+	filename string
 }
 
 func NewHistory() (*History, error) {
 	h := &History{
-		Buf:      arraylist.New[string](),
-		Limit:    100, // resizeme
-		Autosave: true,
-		Enabled:  true,
+		Enabled: true,
+		lines:   arraylist.New[string](),
+		limit:   100, // resizeme
 	}
 
-	err := h.Init()
-	if err != nil {
-		return nil, err
-	}
-
-	return h, nil
-}
-
-func (h *History) Init() error {
 	home, err := os.UserHomeDir()
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	path := filepath.Join(home, ".ollama", "history")
 	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
-		return err
+		return nil, err
 	}
 
-	h.Filename = path
+	h.filename = path
 
 	f, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0o600)
 	if err != nil {
-		if errors.Is(err, os.ErrNotExist) {
-			return nil
-		}
-		return err
+		return nil, err
 	}
 	defer f.Close()
 
-	r := bufio.NewReader(f)
-	for {
-		line, err := r.ReadString('\n')
-		if err != nil {
-			if errors.Is(err, io.EOF) {
-				break
-			}
-			return err
-		}
-
-		line = strings.TrimSpace(line)
-		if len(line) == 0 {
-			continue
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		if line := strings.TrimSpace(scanner.Text()); len(line) > 0 {
+			h.Add(line)
 		}
-
-		h.Add(line)
 	}
 
-	return nil
+	return h, nil
 }
 
 func (h *History) Add(s string) {
-	if latest, _ := h.Buf.Get(h.Size() - 1); latest != s {
-		h.Buf.Add(s)
+	if latest, _ := h.lines.Get(h.Size() - 1); latest != s {
+		h.lines.Add(s)
 		h.Compact()
-		h.Pos = h.Size()
-		if h.Autosave {
-			_ = h.Save()
-		}
+		_ = h.Save()
 	}
+	// always set position to the end
+	h.pos = h.Size()
 }
 
 func (h *History) Compact() {
-	s := h.Buf.Size()
-	if s > h.Limit {
-		for range s - h.Limit {
-			h.Buf.Remove(0)
+	if s := h.lines.Size(); s > h.limit {
+		for range s - h.limit {
+			h.lines.Remove(0)
 		}
 	}
 }
 
 func (h *History) Clear() {
-	h.Buf.Clear()
+	h.lines.Clear()
 }
 
 func (h *History) Prev() (line string) {
-	if h.Pos > 0 {
-		h.Pos -= 1
+	if h.pos > 0 {
+		h.pos -= 1
 	}
-	line, _ = h.Buf.Get(h.Pos)
+	// return first line if at the beginning
+	line, _ = h.lines.Get(h.pos)
 	return line
 }
 
 func (h *History) Next() (line string) {
-	if h.Pos < h.Buf.Size() {
-		h.Pos += 1
-		line, _ = h.Buf.Get(h.Pos)
+	if h.pos < h.lines.Size() {
+		h.pos += 1
+		line, _ = h.lines.Get(h.pos)
 	}
+	// return empty string if at the end
 	return line
 }
 
 func (h *History) Size() int {
-	return h.Buf.Size()
+	return h.lines.Size()
 }
 
 func (h *History) Save() error {
@@ -129,25 +103,21 @@ func (h *History) Save() error {
 		return nil
 	}
 
-	tmpFile := h.Filename + ".tmp"
-
-	f, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0o600)
+	f, err := os.CreateTemp(filepath.Dir(h.filename), "")
 	if err != nil {
 		return err
 	}
-	defer f.Close()
 
-	buf := bufio.NewWriter(f)
-	for cnt := range h.Size() {
-		line, _ := h.Buf.Get(cnt)
-		fmt.Fprintln(buf, line)
-	}
-	buf.Flush()
-	f.Close()
+	func() {
+		defer f.Close()
 
-	if err = os.Rename(tmpFile, h.Filename); err != nil {
-		return err
-	}
+		w := bufio.NewWriter(f)
+		defer w.Flush()
+
+		h.lines.Each(func(i int, line string) {
+			fmt.Fprintln(w, line)
+		})
+	}()
 
-	return nil
+	return os.Rename(f.Name(), h.filename)
 }

+ 2 - 14
readline/readline.go

@@ -91,8 +91,6 @@ func (i *Instance) Readline() (string, error) {
 	var escex bool
 	var metaDel bool
 
-	var currentLineBuf []rune
-
 	for {
 		// don't show placeholder when pasting unless we're in multiline mode
 		showPlaceholder := !i.Pasting || i.Prompt.UseAlt
@@ -116,19 +114,9 @@ func (i *Instance) Readline() (string, error) {
 
 			switch r {
 			case KeyUp:
-				if i.History.Pos > 0 {
-					if i.History.Pos == i.History.Size() {
-						currentLineBuf = []rune(buf.String())
-					}
-					buf.Replace([]rune(i.History.Prev()))
-				}
+				buf.Replace([]rune(i.History.Prev()))
 			case KeyDown:
-				if i.History.Pos < i.History.Size() {
-					buf.Replace([]rune(i.History.Next()))
-					if i.History.Pos == i.History.Size() {
-						buf.Replace(currentLineBuf)
-					}
-				}
+				buf.Replace([]rune(i.History.Next()))
 			case KeyLeft:
 				buf.MoveLeft()
 			case KeyRight: