From 8627f6c66cb0246bb3952cf490fdf44a50edce20 Mon Sep 17 00:00:00 2001 From: Patrick Devine Date: Tue, 14 Nov 2023 15:59:35 -0800 Subject: [PATCH] initial commit of the readline editor replacement --- cmd/cmd.go | 40 +- editor/buffer.go | 488 +++++++++++++++++++++++ readline/readline.go => editor/editor.go | 97 ++--- {readline => editor}/errors.go | 2 +- {readline => editor}/term.go | 2 +- {readline => editor}/term_bsd.go | 3 +- {readline => editor}/term_linux.go | 3 +- editor/term_windows.go | 43 ++ {readline => editor}/types.go | 2 +- readline/buffer.go | 372 ----------------- readline/history.go | 152 ------- readline/term_windows.go | 62 --- 12 files changed, 570 insertions(+), 696 deletions(-) create mode 100644 editor/buffer.go rename readline/readline.go => editor/editor.go (65%) rename {readline => editor}/errors.go (91%) rename {readline => editor}/term.go (98%) rename {readline => editor}/term_bsd.go (96%) rename {readline => editor}/term_linux.go (96%) create mode 100644 editor/term_windows.go rename {readline => editor}/types.go (98%) delete mode 100644 readline/buffer.go delete mode 100644 readline/history.go delete mode 100644 readline/term_windows.go diff --git a/cmd/cmd.go b/cmd/cmd.go index d78395587..c976ac474 100644 --- a/cmd/cmd.go +++ b/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.") diff --git a/editor/buffer.go b/editor/buffer.go new file mode 100644 index 000000000..ef0241853 --- /dev/null +++ b/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() +} diff --git a/readline/readline.go b/editor/editor.go similarity index 65% rename from readline/readline.go rename to editor/editor.go index d3fcc5379..6de4b4e72 100644 --- a/readline/readline.go +++ b/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) { diff --git a/readline/errors.go b/editor/errors.go similarity index 91% rename from readline/errors.go rename to editor/errors.go index 40e40cb77..8f1d720d1 100644 --- a/readline/errors.go +++ b/editor/errors.go @@ -1,4 +1,4 @@ -package readline +package editor import ( "errors" diff --git a/readline/term.go b/editor/term.go similarity index 98% rename from readline/term.go rename to editor/term.go index 45757e6a9..f23fd98d9 100644 --- a/readline/term.go +++ b/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" diff --git a/readline/term_bsd.go b/editor/term_bsd.go similarity index 96% rename from readline/term_bsd.go rename to editor/term_bsd.go index 04b912f04..89ec1e91d 100644 --- a/readline/term_bsd.go +++ b/editor/term_bsd.go @@ -1,6 +1,5 @@ //go:build darwin || freebsd || netbsd || openbsd - -package readline +package editor import ( "syscall" diff --git a/readline/term_linux.go b/editor/term_linux.go similarity index 96% rename from readline/term_linux.go rename to editor/term_linux.go index 2d6211dd6..f615ef858 100644 --- a/readline/term_linux.go +++ b/editor/term_linux.go @@ -1,6 +1,5 @@ //go:build linux || solaris - -package readline +package editor import ( "syscall" diff --git a/editor/term_windows.go b/editor/term_windows.go new file mode 100644 index 000000000..eb63f2239 --- /dev/null +++ b/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 +} diff --git a/readline/types.go b/editor/types.go similarity index 98% rename from readline/types.go rename to editor/types.go index 03fa526d3..f920f5379 100644 --- a/readline/types.go +++ b/editor/types.go @@ -1,4 +1,4 @@ -package readline +package editor const ( CharNull = 0 diff --git a/readline/buffer.go b/readline/buffer.go deleted file mode 100644 index d66b512a7..000000000 --- a/readline/buffer.go +++ /dev/null @@ -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) -} diff --git a/readline/history.go b/readline/history.go deleted file mode 100644 index 5a7c34e27..000000000 --- a/readline/history.go +++ /dev/null @@ -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 -} diff --git a/readline/term_windows.go b/readline/term_windows.go deleted file mode 100644 index 3d1c80e19..000000000 --- a/readline/term_windows.go +++ /dev/null @@ -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 -}