chore: clean up readline package

This commit is contained in:
Michael Yang 2024-12-21 00:21:23 -08:00
parent 7ccdc98a5f
commit a1d90e68d0
3 changed files with 204 additions and 267 deletions

View File

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

View File

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

View File

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