Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
ad83c87454 | ||
|
8627f6c66c |
40
cmd/cmd.go
40
cmd/cmd.go
@ -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
488
editor/buffer.go
Normal 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()
|
||||
}
|
@ -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) {
|
@ -1,4 +1,4 @@
|
||||
package readline
|
||||
package editor
|
||||
|
||||
import (
|
||||
"errors"
|
@ -1,6 +1,6 @@
|
||||
//go:build aix || darwin || dragonfly || freebsd || (linux && !appengine) || netbsd || openbsd || os400 || solaris
|
||||
|
||||
package readline
|
||||
package editor
|
||||
|
||||
import (
|
||||
"syscall"
|
@ -1,6 +1,5 @@
|
||||
//go:build darwin || freebsd || netbsd || openbsd
|
||||
|
||||
package readline
|
||||
package editor
|
||||
|
||||
import (
|
||||
"syscall"
|
@ -1,6 +1,5 @@
|
||||
//go:build linux || solaris
|
||||
|
||||
package readline
|
||||
package editor
|
||||
|
||||
import (
|
||||
"syscall"
|
@ -1,4 +1,4 @@
|
||||
package readline
|
||||
package editor
|
||||
|
||||
const (
|
||||
CharNull = 0
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user