Compare commits

...

2 Commits
main ... editor

Author SHA1 Message Date
Patrick Devine
ad83c87454 add back in the windows terminal file 2023-11-14 16:52:34 -08:00
Patrick Devine
8627f6c66c initial commit of the readline editor replacement 2023-11-14 15:59:35 -08:00
11 changed files with 527 additions and 634 deletions

View File

@ -27,9 +27,9 @@ import (
"golang.org/x/term"
"github.com/jmorganca/ollama/api"
"github.com/jmorganca/ollama/editor"
"github.com/jmorganca/ollama/format"
"github.com/jmorganca/ollama/progressbar"
"github.com/jmorganca/ollama/readline"
"github.com/jmorganca/ollama/server"
"github.com/jmorganca/ollama/version"
)
@ -539,30 +539,24 @@ func generateInteractive(cmd *cobra.Command, model string, wordWrap bool, format
fmt.Fprintln(os.Stderr, "")
}
prompt := readline.Prompt{
Prompt: ">>> ",
AltPrompt: "... ",
Placeholder: "Send a message (/? for help)",
AltPlaceholder: `Use """ to end multi-line input`,
prompt := editor.Prompt{
Prompt: ">>> ",
AltPrompt: "... ",
Placeholder: "Send a message (/? for help)",
}
scanner, err := readline.New(prompt)
ed, err := editor.New(prompt)
if err != nil {
return err
}
fmt.Print(readline.StartBracketedPaste)
defer fmt.Printf(readline.EndBracketedPaste)
var multiLineBuffer string
for {
line, err := scanner.Readline()
line, err := ed.HandleInput()
switch {
case errors.Is(err, io.EOF):
fmt.Println()
return nil
case errors.Is(err, readline.ErrInterrupt):
case errors.Is(err, editor.ErrInterrupt):
if line == "" {
fmt.Println("\nUse Ctrl-D or /bye to exit.")
}
@ -575,20 +569,6 @@ func generateInteractive(cmd *cobra.Command, model string, wordWrap bool, format
line = strings.TrimSpace(line)
switch {
case scanner.Prompt.UseAlt:
if strings.HasSuffix(line, `"""`) {
scanner.Prompt.UseAlt = false
multiLineBuffer += strings.TrimSuffix(line, `"""`)
line = multiLineBuffer
multiLineBuffer = ""
} else {
multiLineBuffer += line + " "
continue
}
case strings.HasPrefix(line, `"""`):
scanner.Prompt.UseAlt = true
multiLineBuffer = strings.TrimPrefix(line, `"""`) + " "
continue
case strings.HasPrefix(line, "/list"):
args := strings.Fields(line)
if err := ListHandler(cmd, args[1:]); err != nil {
@ -599,9 +579,9 @@ func generateInteractive(cmd *cobra.Command, model string, wordWrap bool, format
if len(args) > 1 {
switch args[1] {
case "history":
scanner.HistoryEnable()
//scanner.HistoryEnable()
case "nohistory":
scanner.HistoryDisable()
//scanner.HistoryDisable()
case "wordwrap":
wordWrap = true
fmt.Println("Set 'wordwrap' mode.")

488
editor/buffer.go Normal file
View File

@ -0,0 +1,488 @@
package editor
import (
"fmt"
"strings"
"github.com/emirpasic/gods/lists/arraylist"
"golang.org/x/term"
)
type Buffer struct {
PosX int
PosY int
Buf []*arraylist.List
Prompt *Prompt
WordWrap int
ScreenWidth int
ScreenHeight int
}
func NewBuffer(prompt *Prompt) (*Buffer, error) {
width, height, err := term.GetSize(0)
if err != nil {
fmt.Println("Error getting size:", err)
return nil, err
}
b := &Buffer{
PosX: 0,
PosY: 0,
Buf: []*arraylist.List{arraylist.New()},
Prompt: prompt,
ScreenWidth: width,
ScreenHeight: height,
}
return b, nil
}
func (b *Buffer) LineWidth() int {
return b.ScreenWidth - len(b.Prompt.Prompt)
}
func (b *Buffer) findWordAtPos(line string, pos int) string {
return ""
}
func (b *Buffer) addLine(row int) {
if row+1 == len(b.Buf) {
b.Buf = append(b.Buf, arraylist.New())
} else {
b.Buf = append(b.Buf, nil)
copy(b.Buf[row+2:], b.Buf[row+1:])
b.Buf[row+1] = arraylist.New()
}
}
func (b *Buffer) Add(r rune) {
switch r {
case CharCtrlJ, CharEnter:
b.addLine(b.PosY)
// handle Ctrl-J in the middle of a line
var remainingText string
if b.PosX < b.Buf[b.PosY].Size() {
fmt.Print(ClearToEOL)
remainingText = b.StringLine(b.PosX, b.PosY)
for cnt := 0; cnt < len(remainingText); cnt++ {
b.Buf[b.PosY].Remove(b.Buf[b.PosY].Size() - 1)
b.Buf[b.PosY+1].Add(rune(remainingText[cnt]))
}
}
b.PosY++
b.PosX = 0
fmt.Printf("\n... " + ClearToEOL)
b.drawRemaining()
default:
if b.PosX == b.Buf[b.PosY].Size() {
fmt.Printf("%c", r)
b.PosX++
b.Buf[b.PosY].Add(r)
wrap, prefix, offset := b.splitLineInsert(b.PosY, b.PosX)
if wrap {
fmt.Print(CursorHide + cursorLeftN(len(prefix)+1) + ClearToEOL)
fmt.Printf("\n%s... %s%c", ClearToEOL, prefix, r)
b.PosY++
b.PosX = offset
b.ResetCursor()
b.drawRemaining()
fmt.Print(CursorShow)
}
} else {
fmt.Printf("%c", r)
b.Buf[b.PosY].Insert(b.PosX, r)
b.PosX++
_, prefix, offset := b.splitLineInsert(b.PosY, b.PosX)
fmt.Print(CursorHide)
if b.PosX > b.Buf[b.PosY].Size() {
if offset > 0 {
fmt.Print(cursorLeftN(offset))
}
fmt.Print(ClearToEOL + CursorDown + CursorBOL + ClearToEOL)
fmt.Printf("... %s", prefix[:offset])
b.PosY++
b.PosX = offset
b.ResetCursor()
}
b.drawRemaining()
fmt.Print(CursorShow)
}
}
}
func (b *Buffer) ResetCursor() {
fmt.Print(CursorHide + CursorBOL)
fmt.Print(cursorRightN(b.PosX + len(b.Prompt.Prompt)))
fmt.Print(CursorShow)
}
func (b *Buffer) splitLineInsert(posY, posX int) (bool, string, int) {
line := b.StringLine(0, posY)
screenEdge := b.LineWidth() - 5
// if the current line doesn't need to be reflowed, none of the other
// lines will either
if len(line) <= screenEdge {
return false, "", 0
}
// we know we're going to have to insert onto the next line, so
// add another line if there isn't one already
if posY == len(b.Buf)-1 {
b.Buf = append(b.Buf, arraylist.New())
}
// make a truncated version of the current line
currLine := line[:screenEdge]
// figure out where the last space in the line is
idx := strings.LastIndex(currLine, " ")
// deal with strings that don't have spaces in them
if idx == -1 {
idx = len(currLine) - 1
}
// if the next line already has text on it, we need
// to add a space to insert our new word
if b.Buf[posY+1].Size() > 0 {
b.Buf[posY+1].Insert(0, ' ')
}
// calculate the number of characters we need to remove
// from the current line to add to the next one
totalChars := len(line) - idx - 1
for cnt := 0; cnt < totalChars; cnt++ {
b.Buf[posY].Remove(b.Buf[posY].Size() - 1)
b.Buf[posY+1].Insert(0, rune(line[len(line)-1-cnt]))
}
// remove the trailing space
b.Buf[posY].Remove(b.Buf[posY].Size() - 1)
// wrap any further lines
if b.Buf[posY+1].Size() > b.LineWidth()-5 {
b.splitLineInsert(posY+1, 0)
}
return true, currLine[idx+1:], posX - idx - 1
}
func (b *Buffer) drawRemaining() {
remainingText := b.StringFromRow(b.PosY)
remainingText = remainingText[b.PosX:]
fmt.Print(CursorHide + ClearToEOL)
var rowCount int
for _, c := range remainingText {
fmt.Print(string(c))
if c == '\n' {
fmt.Print("... " + ClearToEOL)
rowCount++
}
}
if rowCount > 0 {
fmt.Print(cursorUpN(rowCount))
}
b.ResetCursor()
}
func (b *Buffer) findWordBeginning(posX int) int {
for {
if posX < 0 {
return -1
}
r, ok := b.Buf[b.PosY].Get(posX)
if !ok {
return -1
} else if r.(rune) == ' ' {
return posX
}
posX--
}
}
func (b *Buffer) Delete() {
if b.PosX < b.Buf[b.PosY].Size()-1 {
b.Buf[b.PosY].Remove(b.PosX)
b.drawRemaining()
} else {
b.joinLines()
}
}
func (b *Buffer) joinLines() {
lineLen := b.Buf[b.PosY].Size()
for cnt := 0; cnt < lineLen; cnt++ {
r, _ := b.Buf[b.PosY].Get(0)
b.Buf[b.PosY].Remove(0)
b.Buf[b.PosY-1].Add(r)
}
}
func (b *Buffer) Remove() {
if b.PosX > 0 {
fmt.Print(CursorLeft + " " + CursorLeft)
b.PosX--
b.Buf[b.PosY].Remove(b.PosX)
if b.PosX < b.Buf[b.PosY].Size() {
fmt.Print(ClearToEOL)
b.drawRemaining()
}
} else if b.PosX == 0 && b.PosY > 0 {
b.joinLines()
lastPos := b.Buf[b.PosY-1].Size()
var cnt int
b.PosX = lastPos
b.PosY--
fmt.Print(CursorHide)
for {
if b.PosX+cnt > b.LineWidth()-5 {
// the concatenated line won't fit, so find the beginning of the word
// and copy the rest of the string from there
idx := b.findWordBeginning(b.PosX)
lineLen := b.Buf[b.PosY].Size()
for offset := idx + 1; offset < lineLen; offset++ {
r, _ := b.Buf[b.PosY].Get(idx + 1)
b.Buf[b.PosY].Remove(idx + 1)
b.Buf[b.PosY+1].Add(r)
}
// remove the trailing space
b.Buf[b.PosY].Remove(idx)
fmt.Print(CursorUp + ClearToEOL)
b.PosX = 0
b.drawRemaining()
fmt.Print(CursorDown)
if idx > 0 {
if lastPos-idx-1 > 0 {
b.PosX = lastPos - idx - 1
b.ResetCursor()
}
}
b.PosY++
break
}
r, ok := b.Buf[b.PosY].Get(b.PosX + cnt)
if !ok {
// found the end of the string
fmt.Print(CursorUp + cursorRightN(b.PosX) + ClearToEOL)
b.drawRemaining()
break
}
if r == ' ' {
// found the end of the word
lineLen := b.Buf[b.PosY].Size()
for offset := b.PosX + cnt + 1; offset < lineLen; offset++ {
r, _ := b.Buf[b.PosY].Get(b.PosX + cnt + 1)
b.Buf[b.PosY].Remove(b.PosX + cnt + 1)
b.Buf[b.PosY+1].Add(r)
}
fmt.Print(CursorUp + cursorRightN(b.PosX) + ClearToEOL)
b.drawRemaining()
break
}
cnt++
}
fmt.Print(CursorShow)
}
}
func (b *Buffer) RemoveBefore() {
for {
if b.PosX == 0 && b.PosY == 0 {
break
}
b.Remove()
}
}
func (b *Buffer) RemoveWordBefore() {
if b.PosX > 0 || b.PosY > 0 {
var foundNonspace bool
for {
xPos := b.PosX
yPos := b.PosY
v, _ := b.Buf[yPos].Get(xPos - 1)
if v == ' ' {
if !foundNonspace {
b.Remove()
} else {
break
}
} else {
foundNonspace = true
b.Remove()
}
if xPos == 0 && yPos == 0 {
break
}
}
}
}
func (b *Buffer) StringLine(x, y int) string {
if y >= len(b.Buf) {
return ""
}
var output string
for cnt := x; cnt < b.Buf[y].Size(); cnt++ {
r, _ := b.Buf[y].Get(cnt)
output += string(r.(rune))
}
return output
}
func (b *Buffer) String() string {
return b.StringFromRow(0)
}
func (b *Buffer) StringFromRow(n int) string {
var output []string
for _, row := range b.Buf[n:] {
var currLine string
for cnt := 0; cnt < row.Size(); cnt++ {
r, _ := row.Get(cnt)
currLine += string(r.(rune))
}
currLine = strings.TrimRight(currLine, " ")
output = append(output, currLine)
}
return strings.Join(output, "\n")
}
func (b *Buffer) cursorUp() {
fmt.Print(CursorUp)
b.ResetCursor()
}
func (b *Buffer) cursorDown() {
fmt.Print(CursorDown)
b.ResetCursor()
}
func (b *Buffer) MoveUp() {
if b.PosY > 0 {
b.PosY--
if b.Buf[b.PosY].Size() < b.PosX {
b.PosX = b.Buf[b.PosY].Size()
}
b.cursorUp()
} else {
fmt.Print("\a")
}
}
func (b *Buffer) MoveDown() {
if b.PosY < len(b.Buf)-1 {
b.PosY++
if b.Buf[b.PosY].Size() < b.PosX {
b.PosX = b.Buf[b.PosY].Size()
}
b.cursorDown()
} else {
fmt.Print("\a")
}
}
func (b *Buffer) MoveLeft() {
if b.PosX > 0 {
b.PosX--
fmt.Print(CursorLeft)
} else if b.PosY > 0 {
b.PosX = b.Buf[b.PosY-1].Size()
b.PosY--
b.cursorUp()
} else if b.PosX == 0 && b.PosY == 0 {
fmt.Print("\a")
}
}
func (b *Buffer) MoveRight() {
if b.PosX < b.Buf[b.PosY].Size() {
b.PosX++
fmt.Print(CursorRight)
} else if b.PosY < len(b.Buf)-1 {
b.PosY++
b.PosX = 0
b.cursorDown()
} else {
fmt.Print("\a")
}
}
func (b *Buffer) MoveToBOL() {
if b.PosX > 0 {
b.PosX = 0
b.ResetCursor()
}
}
func (b *Buffer) MoveToEOL() {
if b.PosX < b.Buf[b.PosY].Size() {
b.PosX = b.Buf[b.PosY].Size()
b.ResetCursor()
}
}
func (b *Buffer) MoveToEnd() {
fmt.Print(CursorHide)
yDiff := len(b.Buf)-1 - b.PosY
if yDiff > 0 {
fmt.Print(cursorDownN(yDiff))
}
b.PosY = len(b.Buf)-1
b.MoveToEOL()
fmt.Print(CursorShow)
}
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)
}
func (b *Buffer) ClearScreen() {
fmt.Printf(CursorHide + ClearScreen + CursorReset + b.Prompt.Prompt)
if b.IsEmpty() {
ph := b.Prompt.Placeholder
fmt.Printf(ColorGrey + ph + cursorLeftN(len(ph)) + ColorDefault)
} else {
currPosX := b.PosX
currPosY := b.PosY
b.PosX = 0
b.PosY = 0
b.drawRemaining()
b.PosX = currPosX
b.PosY = currPosY
fmt.Print(CursorReset + cursorRightN(len(b.Prompt.Prompt)))
if b.PosY > 0 {
fmt.Print(cursorDownN(b.PosY))
}
if b.PosX > 0 {
fmt.Print(cursorRightN(b.PosX))
}
}
fmt.Print(CursorShow)
}
func (b *Buffer) IsEmpty() bool {
return len(b.Buf) == 1 && b.Buf[0].Empty()
}

View File

@ -1,4 +1,4 @@
package readline
package editor
import (
"bufio"
@ -23,7 +23,6 @@ type Terminal struct {
type Instance struct {
Prompt *Prompt
Terminal *Terminal
History *History
}
func New(prompt Prompt) (*Instance, error) {
@ -32,40 +31,33 @@ func New(prompt Prompt) (*Instance, error) {
return nil, err
}
history, err := NewHistory()
if err != nil {
return nil, err
}
return &Instance{
Prompt: &prompt,
Terminal: term,
History: history,
}, nil
}
func (i *Instance) Readline() (string, error) {
func (i *Instance) HandleInput() (string, error) {
prompt := i.Prompt.Prompt
if i.Prompt.UseAlt {
prompt = i.Prompt.AltPrompt
}
fmt.Print(prompt)
fd := int(syscall.Stdin)
termios, err := SetRawMode(fd)
termios, err := SetRawMode(syscall.Stdin)
if err != nil {
return "", err
}
defer UnsetRawMode(fd, termios)
defer UnsetRawMode(syscall.Stdin, termios)
buf, _ := NewBuffer(i.Prompt)
var esc bool
var escex bool
var metaDel bool
var pasteMode PasteMode
var currentLineBuf []rune
fmt.Print(StartBracketedPaste)
defer fmt.Printf(EndBracketedPaste)
for {
if buf.IsEmpty() {
@ -77,33 +69,22 @@ func (i *Instance) Readline() (string, error) {
}
r, err := i.Terminal.Read()
if err != nil {
return "", io.EOF
}
if buf.IsEmpty() {
fmt.Print(ClearToEOL)
}
if err != nil {
return "", io.EOF
}
if escex {
escex = false
switch r {
case KeyUp:
if i.History.Pos > 0 {
if i.History.Pos == i.History.Size() {
currentLineBuf = []rune(buf.String())
}
buf.Replace(i.History.Prev())
}
buf.MoveUp()
case KeyDown:
if i.History.Pos < i.History.Size() {
buf.Replace(i.History.Next())
if i.History.Pos == i.History.Size() {
buf.Replace(currentLineBuf)
}
}
buf.MoveDown()
case KeyLeft:
buf.MoveLeft()
case KeyRight:
@ -123,28 +104,16 @@ func (i *Instance) Readline() (string, error) {
} else if code == CharBracketedPasteEnd {
pasteMode = PasteModeEnd
}
case KeyDel:
if buf.Size() > 0 {
buf.Delete()
}
metaDel = true
case MetaStart:
buf.MoveToStart()
buf.MoveToBOL()
case MetaEnd:
buf.MoveToEnd()
default:
// skip any keys we don't know about
continue
buf.MoveToEOL()
}
continue
} else if esc {
esc = false
switch r {
case 'b':
buf.MoveLeftWord()
case 'f':
buf.MoveRightWord()
case CharEscapeEx:
escex = true
}
@ -159,9 +128,9 @@ func (i *Instance) Readline() (string, error) {
case CharInterrupt:
return "", ErrInterrupt
case CharLineStart:
buf.MoveToStart()
buf.MoveToBOL()
case CharLineEnd:
buf.MoveToEnd()
buf.MoveToEOL()
case CharBackward:
buf.MoveLeft()
case CharForward:
@ -169,56 +138,38 @@ func (i *Instance) Readline() (string, error) {
case CharBackspace, CharCtrlH:
buf.Remove()
case CharTab:
// todo: convert back to real tabs
for cnt := 0; cnt < 8; cnt++ {
buf.Add(' ')
}
case CharDelete:
if buf.Size() > 0 {
if len(buf.Buf) > 0 && buf.Buf[0].Size() > 0 {
buf.Delete()
} else {
return "", io.EOF
}
case CharKill:
buf.DeleteRemaining()
case CharCtrlU:
buf.DeleteBefore()
buf.RemoveBefore()
case CharCtrlL:
buf.ClearScreen()
case CharCtrlW:
buf.DeleteWord()
buf.RemoveWordBefore()
case CharCtrlJ:
buf.Add(r)
case CharEnter:
output := buf.String()
if output != "" {
i.History.Add([]rune(output))
if pasteMode == PasteModeStart {
buf.Add(r)
continue
}
buf.MoveToEnd()
fmt.Println()
switch pasteMode {
case PasteModeStart:
output = `"""` + output
case PasteModeEnd:
output = output + `"""`
}
return output, nil
return buf.String(), nil
default:
if metaDel {
metaDel = false
continue
}
if r >= CharSpace || r == CharEnter {
buf.Add(r)
}
}
}
}
func (i *Instance) HistoryEnable() {
i.History.Enabled = true
}
func (i *Instance) HistoryDisable() {
i.History.Enabled = false
}
func NewTerminal() (*Terminal, error) {

View File

@ -1,4 +1,4 @@
package readline
package editor
import (
"errors"

View File

@ -1,6 +1,6 @@
//go:build aix || darwin || dragonfly || freebsd || (linux && !appengine) || netbsd || openbsd || os400 || solaris
package readline
package editor
import (
"syscall"

View File

@ -1,6 +1,5 @@
//go:build darwin || freebsd || netbsd || openbsd
package readline
package editor
import (
"syscall"

View File

@ -1,6 +1,5 @@
//go:build linux || solaris
package readline
package editor
import (
"syscall"

View File

@ -1,4 +1,4 @@
package readline
package editor
const (
CharNull = 0

View File

@ -1,372 +0,0 @@
package readline
import (
"fmt"
"os"
"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) {
fd := int(os.Stdout.Fd())
width, height, err := term.GetSize(fd)
if err != nil {
fmt.Println("Error getting size:", err)
return nil, err
}
lwidth := width - len(prompt.Prompt)
if prompt.UseAlt {
lwidth = width - len(prompt.AltPrompt)
}
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 {
fmt.Print(CursorLeft)
}
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 {
fmt.Printf(CursorDown + CursorBOL + cursorRightN(b.PromptSize()))
} else {
fmt.Print(CursorRight)
}
}
}
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++ {
fmt.Print(CursorUp)
}
}
fmt.Printf(CursorBOL + cursorRightN(b.PromptSize()))
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++ {
fmt.Print(CursorDown)
}
remainder := b.Size() % b.LineWidth
fmt.Printf(CursorBOL + cursorRightN(b.PromptSize()+remainder))
} else {
fmt.Print(cursorRightN(b.Size() - b.Pos))
}
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) PromptSize() int {
if b.Prompt.UseAlt {
return len(b.Prompt.AltPrompt)
}
return len(b.Prompt.Prompt)
}
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
}
fmt.Print(CursorHide)
// render the rest of the current line
currLine := remainingText[:min(b.LineWidth-place, len(remainingText))]
if len(currLine) > 0 {
fmt.Printf(ClearToEOL + currLine)
fmt.Print(cursorLeftN(len(currLine)))
} else {
fmt.Print(ClearToEOL)
}
// 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)
}
fmt.Print(ClearToEOL)
fmt.Print(cursorUpN(totalLines))
fmt.Printf(CursorBOL + cursorRightN(b.Width-len(currLine)))
}
fmt.Print(CursorShow)
}
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
fmt.Printf(cursorUpN(remainingLines+1) + cursorRightN(place+len(b.Prompt.Prompt)))
}
}
}
}
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
fmt.Printf(cursorUpN(remainingLines) + cursorRightN(place+len(b.Prompt.Prompt)))
}
}
}
}
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() {
fmt.Printf(ClearScreen + CursorReset + b.Prompt.Prompt)
if b.IsEmpty() {
ph := b.Prompt.Placeholder
fmt.Printf(ColorGrey + ph + cursorLeftN(len(ph)) + ColorDefault)
} else {
currPos := b.Pos
b.Pos = 0
b.drawRemaining()
fmt.Printf(CursorReset + cursorRightN(len(b.Prompt.Prompt)))
if currPos > 0 {
targetLine := currPos / b.LineWidth
if targetLine > 0 {
for cnt := 0; cnt < targetLine; cnt++ {
fmt.Print(CursorDown)
}
}
remainder := currPos % b.LineWidth
if remainder > 0 {
fmt.Print(cursorRightN(remainder))
}
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()
fmt.Printf(ClearLine + CursorBOL + b.Prompt.Prompt)
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)
}

View File

@ -1,152 +0,0 @@
package readline
import (
"bufio"
"errors"
"io"
"os"
"path/filepath"
"strings"
"github.com/emirpasic/gods/lists/arraylist"
)
type History struct {
Buf *arraylist.List
Autosave bool
Pos int
Limit int
Filename string
Enabled bool
}
func NewHistory() (*History, error) {
h := &History{
Buf: arraylist.New(),
Limit: 100, //resizeme
Autosave: true,
Enabled: true,
}
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
}
path := filepath.Join(home, ".ollama", "history")
h.Filename = path
//todo check if the file exists
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0600)
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 err == io.EOF {
break
}
return err
}
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
h.Add([]rune(line))
}
return nil
}
func (h *History) Add(l []rune) {
h.Buf.Add(l)
h.Compact()
h.Pos = h.Size()
if h.Autosave {
h.Save()
}
}
func (h *History) Compact() {
s := h.Buf.Size()
if s > h.Limit {
for cnt := 0; cnt < s-h.Limit; cnt++ {
h.Buf.Remove(0)
}
}
}
func (h *History) Clear() {
h.Buf.Clear()
}
func (h *History) Prev() []rune {
var line []rune
if h.Pos > 0 {
h.Pos -= 1
}
v, _ := h.Buf.Get(h.Pos)
line, _ = v.([]rune)
return line
}
func (h *History) Next() []rune {
var line []rune
if h.Pos < h.Buf.Size() {
h.Pos += 1
v, _ := h.Buf.Get(h.Pos)
line, _ = v.([]rune)
}
return line
}
func (h *History) Size() int {
return h.Buf.Size()
}
func (h *History) Save() error {
if !h.Enabled {
return nil
}
tmpFile := h.Filename + ".tmp"
f, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666)
if err != nil {
return err
}
defer f.Close()
buf := bufio.NewWriter(f)
for cnt := 0; cnt < h.Size(); cnt++ {
v, _ := h.Buf.Get(cnt)
line, _ := v.([]rune)
buf.WriteString(string(line) + "\n")
}
buf.Flush()
f.Close()
if err = os.Rename(tmpFile, h.Filename); err != nil {
return err
}
return nil
}