Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Yang
a1d90e68d0 chore: clean up readline package 2024-12-21 01:12:17 -08:00
Michael Yang
7ccdc98a5f fix: only add to history of different
if the last item in history is the same as the one being added, skip it.
this reduces the number of history entries. the behaviour is similar to
how most shells maintain history
2024-12-21 00:08:14 -08:00
3 changed files with 204 additions and 265 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,113 +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) {
h.Buf.Add(s) if latest, _ := h.lines.Get(h.Size() - 1); latest != s {
h.Compact() h.lines.Add(s)
h.Pos = h.Size() h.Compact()
if h.Autosave {
_ = h.Save() _ = 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 {
@ -127,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: