Browse Source

initial commit of the readline editor replacement

Patrick Devine 1 năm trước cách đây
mục cha
commit
8627f6c66c
12 tập tin đã thay đổi với 570 bổ sung696 xóa
  1. 10 30
      cmd/cmd.go
  2. 488 0
      editor/buffer.go
  3. 24 73
      editor/editor.go
  4. 1 1
      editor/errors.go
  5. 1 1
      editor/term.go
  6. 1 2
      editor/term_bsd.go
  7. 1 2
      editor/term_linux.go
  8. 43 0
      editor/term_windows.go
  9. 1 1
      editor/types.go
  10. 0 372
      readline/buffer.go
  11. 0 152
      readline/history.go
  12. 0 62
      readline/term_windows.go

+ 10 - 30
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 - 0
editor/buffer.go

@@ -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()
+}

+ 24 - 73
readline/readline.go → editor/editor.go

@@ -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 - 1
readline/errors.go → editor/errors.go

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

+ 1 - 1
readline/term.go → editor/term.go

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

+ 1 - 2
readline/term_bsd.go → editor/term_bsd.go

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

+ 1 - 2
readline/term_linux.go → editor/term_linux.go

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

+ 43 - 0
editor/term_windows.go

@@ -0,0 +1,43 @@
+// +build windows
+package editor
+
+import (
+	"syscall"
+	"unsafe"
+)
+
+type State uint32
+
+var kernel32 = syscall.NewLazyDLL("kernel32.dll")
+
+var (
+        procGetConsoleMode             = kernel32.NewProc("GetConsoleMode")
+        procSetConsoleMode             = kernel32.NewProc("SetConsoleMode")
+        procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
+)
+
+// IsTerminal returns true if the given file descriptor is a terminal.
+func IsTerminal(fd int) bool {
+        var st uint32
+        r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
+        return r != 0 && e == 0
+}
+
+func SetRawMode(fd int) (State, err) {
+        var state State
+        _, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&state)), 0)
+        if e != 0 {
+                return 0, error(e)
+        }
+        raw := state &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput)
+        _, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0)
+        if e != 0 {
+                return nil, error(e)
+        }
+        return state, nil
+}
+
+func UnsetRawMode(fd int, state State) error {
+	_, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0)
+	return err
+}

+ 1 - 1
readline/types.go → editor/types.go

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

+ 0 - 372
readline/buffer.go

@@ -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)
-}

+ 0 - 152
readline/history.go

@@ -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
-}

+ 0 - 62
readline/term_windows.go

@@ -1,62 +0,0 @@
-package readline
-
-import (
-	"syscall"
-	"unsafe"
-)
-
-const (
-	enableLineInput       = 2
-	enableWindowInput     = 8
-	enableMouseInput      = 16
-	enableInsertMode      = 32
-	enableQuickEditMode   = 64
-	enableExtendedFlags   = 128
-	enableProcessedOutput = 1
-	enableWrapAtEolOutput = 2
-	enableAutoPosition    = 256 // Cursor position is not affected by writing data to the console.
-	enableEchoInput       = 4   // Characters are written to the console as they're read.
-	enableProcessedInput  = 1   // Enables input processing (like recognizing Ctrl+C).
-)
-
-var kernel32 = syscall.NewLazyDLL("kernel32.dll")
-
-var (
-	procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
-	procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
-)
-
-type State struct {
-	mode uint32
-}
-
-// IsTerminal checks if the given file descriptor is associated with a terminal
-func IsTerminal(fd int) bool {
-	var st uint32
-	r, _, e := syscall.SyscallN(procGetConsoleMode.Addr(), uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
-	// if the call succeeds and doesn't produce an error, it's a terminal
-	return r != 0 && e == 0
-}
-
-func SetRawMode(fd int) (*State, error) {
-	var st uint32
-	// retrieve the current mode of the terminal
-	_, _, e := syscall.SyscallN(procGetConsoleMode.Addr(), uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
-	if e != 0 {
-		return nil, error(e)
-	}
-	// modify the mode to set it to raw
-	raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput)
-	// apply the new mode to the terminal
-	_, _, e = syscall.SyscallN(procSetConsoleMode.Addr(), uintptr(fd), uintptr(raw), 0)
-	if e != 0 {
-		return nil, error(e)
-	}
-	// return the original state so that it can be restored later
-	return &State{st}, nil
-}
-
-func UnsetRawMode(fd int, state *State) error {
-	_, _, err := syscall.SyscallN(procSetConsoleMode.Addr(), uintptr(fd), uintptr(state.mode), 0)
-	return err
-}