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
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 Prompt *Prompt
LineWidth int LineWidth int
Width int Width int
Height int Height int
line *arraylist.List[rune]
spaceMask *arraylist.List[bool]
pos int
displayPos int
} }
func NewBuffer(prompt *Prompt) (*Buffer, error) { func NewBuffer(prompt *Prompt) (*Buffer, error) {
@ -30,58 +30,53 @@ 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 line := b.displayPos/b.LineWidth - 1
hasSpace := b.GetLineSpacing(line) hasSpace := b.GetLineSpacing(line)
if hasSpace { if hasSpace {
b.DisplayPos -= 1 b.displayPos -= 1
fmt.Print(CursorLeft) fmt.Print(CursorLeft)
} }
} else { } else {
fmt.Print(CursorLeftN(rLength)) fmt.Print(CursorLeftN(rLength))
} }
b.Pos -= 1 b.pos -= 1
b.DisplayPos -= rLength b.displayPos -= rLength
}
} }
} }
func (b *Buffer) MoveLeftWord() { func (b *Buffer) MoveLeftWord() {
if b.Pos > 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 {
break break
@ -90,70 +85,58 @@ func (b *Buffer) MoveLeftWord() {
foundNonspace = true foundNonspace = true
} }
b.MoveLeft() b.MoveLeft()
if b.Pos == 0 {
break
}
}
} }
} }
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.Buf.Get(b.Pos) v, _ := b.line.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]
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) { func NewHistory() (*History, error) {
h := &History{ 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() 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.lines.Add(s)
h.Compact() 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() { 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
} }
func() {
defer f.Close() defer f.Close()
buf := bufio.NewWriter(f) w := bufio.NewWriter(f)
for cnt := range h.Size() { defer w.Flush()
line, _ := h.Buf.Get(cnt)
fmt.Fprintln(buf, line)
}
buf.Flush()
f.Close()
if err = os.Rename(tmpFile, h.Filename); err != nil { h.lines.Each(func(i int, line string) {
return err fmt.Fprintln(w, line)
} })
}()
return nil 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 {
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: 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: