mirror of
https://github.com/ollama/ollama.git
synced 2026-03-09 03:12:11 -05:00
feature: add ctrl-g to allow users to use an editor to edit their prompt (#14197)
This commit is contained in:
@@ -2260,7 +2260,7 @@ func NewCLI() *cobra.Command {
|
|||||||
switch cmd {
|
switch cmd {
|
||||||
case runCmd:
|
case runCmd:
|
||||||
imagegen.AppendFlagsDocs(cmd)
|
imagegen.AppendFlagsDocs(cmd)
|
||||||
appendEnvDocs(cmd, []envconfig.EnvVar{envVars["OLLAMA_HOST"], envVars["OLLAMA_NOHISTORY"]})
|
appendEnvDocs(cmd, []envconfig.EnvVar{envVars["OLLAMA_EDITOR"], envVars["OLLAMA_HOST"], envVars["OLLAMA_NOHISTORY"]})
|
||||||
case serveCmd:
|
case serveCmd:
|
||||||
appendEnvDocs(cmd, []envconfig.EnvVar{
|
appendEnvDocs(cmd, []envconfig.EnvVar{
|
||||||
envVars["OLLAMA_DEBUG"],
|
envVars["OLLAMA_DEBUG"],
|
||||||
|
|||||||
5
cmd/editor_unix.go
Normal file
5
cmd/editor_unix.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
const defaultEditor = "vi"
|
||||||
5
cmd/editor_windows.go
Normal file
5
cmd/editor_windows.go
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
const defaultEditor = "edit"
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -79,6 +80,7 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
fmt.Fprintln(os.Stderr, " Ctrl + w Delete the word before the cursor")
|
fmt.Fprintln(os.Stderr, " Ctrl + w Delete the word before the cursor")
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
fmt.Fprintln(os.Stderr, " Ctrl + l Clear the screen")
|
fmt.Fprintln(os.Stderr, " Ctrl + l Clear the screen")
|
||||||
|
fmt.Fprintln(os.Stderr, " Ctrl + g Open default editor to compose a prompt")
|
||||||
fmt.Fprintln(os.Stderr, " Ctrl + c Stop the model from responding")
|
fmt.Fprintln(os.Stderr, " Ctrl + c Stop the model from responding")
|
||||||
fmt.Fprintln(os.Stderr, " Ctrl + d Exit ollama (/bye)")
|
fmt.Fprintln(os.Stderr, " Ctrl + d Exit ollama (/bye)")
|
||||||
fmt.Fprintln(os.Stderr, "")
|
fmt.Fprintln(os.Stderr, "")
|
||||||
@@ -147,6 +149,18 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
|
|||||||
scanner.Prompt.UseAlt = false
|
scanner.Prompt.UseAlt = false
|
||||||
sb.Reset()
|
sb.Reset()
|
||||||
|
|
||||||
|
continue
|
||||||
|
case errors.Is(err, readline.ErrEditPrompt):
|
||||||
|
sb.Reset()
|
||||||
|
content, err := editInExternalEditor(line)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(content) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
scanner.Prefill = content
|
||||||
continue
|
continue
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return err
|
return err
|
||||||
@@ -598,6 +612,57 @@ func extractFileData(input string) (string, []api.ImageData, error) {
|
|||||||
return strings.TrimSpace(input), imgs, nil
|
return strings.TrimSpace(input), imgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func editInExternalEditor(content string) (string, error) {
|
||||||
|
editor := envconfig.Editor()
|
||||||
|
if editor == "" {
|
||||||
|
editor = os.Getenv("VISUAL")
|
||||||
|
}
|
||||||
|
if editor == "" {
|
||||||
|
editor = os.Getenv("EDITOR")
|
||||||
|
}
|
||||||
|
if editor == "" {
|
||||||
|
editor = defaultEditor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the editor binary exists
|
||||||
|
name := strings.Fields(editor)[0]
|
||||||
|
if _, err := exec.LookPath(name); err != nil {
|
||||||
|
return "", fmt.Errorf("editor %q not found, set OLLAMA_EDITOR to the path of your preferred editor", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile, err := os.CreateTemp("", "ollama-prompt-*.txt")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("creating temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
|
||||||
|
if content != "" {
|
||||||
|
if _, err := tmpFile.WriteString(content); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
return "", fmt.Errorf("writing to temp file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
args := strings.Fields(editor)
|
||||||
|
args = append(args, tmpFile.Name())
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", fmt.Errorf("editor exited with error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(tmpFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("reading temp file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimRight(string(data), "\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
func getImageData(filePath string) ([]byte, error) {
|
func getImageData(filePath string) ([]byte, error) {
|
||||||
file, err := os.Open(filePath)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ func String(s string) func() string {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
LLMLibrary = String("OLLAMA_LLM_LIBRARY")
|
LLMLibrary = String("OLLAMA_LLM_LIBRARY")
|
||||||
|
Editor = String("OLLAMA_EDITOR")
|
||||||
|
|
||||||
CudaVisibleDevices = String("CUDA_VISIBLE_DEVICES")
|
CudaVisibleDevices = String("CUDA_VISIBLE_DEVICES")
|
||||||
HipVisibleDevices = String("HIP_VISIBLE_DEVICES")
|
HipVisibleDevices = String("HIP_VISIBLE_DEVICES")
|
||||||
@@ -291,6 +292,7 @@ func AsMap() map[string]EnvVar {
|
|||||||
"OLLAMA_SCHED_SPREAD": {"OLLAMA_SCHED_SPREAD", SchedSpread(), "Always schedule model across all GPUs"},
|
"OLLAMA_SCHED_SPREAD": {"OLLAMA_SCHED_SPREAD", SchedSpread(), "Always schedule model across all GPUs"},
|
||||||
"OLLAMA_MULTIUSER_CACHE": {"OLLAMA_MULTIUSER_CACHE", MultiUserCache(), "Optimize prompt caching for multi-user scenarios"},
|
"OLLAMA_MULTIUSER_CACHE": {"OLLAMA_MULTIUSER_CACHE", MultiUserCache(), "Optimize prompt caching for multi-user scenarios"},
|
||||||
"OLLAMA_CONTEXT_LENGTH": {"OLLAMA_CONTEXT_LENGTH", ContextLength(), "Context length to use unless otherwise specified (default: 4k/32k/256k based on VRAM)"},
|
"OLLAMA_CONTEXT_LENGTH": {"OLLAMA_CONTEXT_LENGTH", ContextLength(), "Context length to use unless otherwise specified (default: 4k/32k/256k based on VRAM)"},
|
||||||
|
"OLLAMA_EDITOR": {"OLLAMA_EDITOR", Editor(), "Path to editor for interactive prompt editing (Ctrl+G)"},
|
||||||
"OLLAMA_NEW_ENGINE": {"OLLAMA_NEW_ENGINE", NewEngine(), "Enable the new Ollama engine"},
|
"OLLAMA_NEW_ENGINE": {"OLLAMA_NEW_ENGINE", NewEngine(), "Enable the new Ollama engine"},
|
||||||
"OLLAMA_REMOTES": {"OLLAMA_REMOTES", Remotes(), "Allowed hosts for remote models (default \"ollama.com\")"},
|
"OLLAMA_REMOTES": {"OLLAMA_REMOTES", Remotes(), "Allowed hosts for remote models (default \"ollama.com\")"},
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var ErrInterrupt = errors.New("Interrupt")
|
var ErrInterrupt = errors.New("Interrupt")
|
||||||
|
var ErrEditPrompt = errors.New("EditPrompt")
|
||||||
|
|
||||||
type InterruptError struct {
|
type InterruptError struct {
|
||||||
Line []rune
|
Line []rune
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Instance struct {
|
|||||||
Terminal *Terminal
|
Terminal *Terminal
|
||||||
History *History
|
History *History
|
||||||
Pasting bool
|
Pasting bool
|
||||||
|
Prefill string
|
||||||
pastedLines []string
|
pastedLines []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +90,27 @@ func (i *Instance) Readline() (string, error) {
|
|||||||
|
|
||||||
buf, _ := NewBuffer(i.Prompt)
|
buf, _ := NewBuffer(i.Prompt)
|
||||||
|
|
||||||
|
// Prefill the buffer with any text that we received from an external editor
|
||||||
|
if i.Prefill != "" {
|
||||||
|
lines := strings.Split(i.Prefill, "\n")
|
||||||
|
i.Prefill = ""
|
||||||
|
for idx, l := range lines {
|
||||||
|
for _, r := range l {
|
||||||
|
buf.Add(r)
|
||||||
|
}
|
||||||
|
if idx < len(lines)-1 {
|
||||||
|
i.pastedLines = append(i.pastedLines, buf.String())
|
||||||
|
buf.Buf.Clear()
|
||||||
|
buf.Pos = 0
|
||||||
|
buf.DisplayPos = 0
|
||||||
|
buf.LineHasSpace.Clear()
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Print(i.Prompt.AltPrompt)
|
||||||
|
i.Prompt.UseAlt = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var esc bool
|
var esc bool
|
||||||
var escex bool
|
var escex bool
|
||||||
var metaDel bool
|
var metaDel bool
|
||||||
@@ -251,6 +273,29 @@ func (i *Instance) Readline() (string, error) {
|
|||||||
buf.ClearScreen()
|
buf.ClearScreen()
|
||||||
case CharCtrlW:
|
case CharCtrlW:
|
||||||
buf.DeleteWord()
|
buf.DeleteWord()
|
||||||
|
case CharBell:
|
||||||
|
output := buf.String()
|
||||||
|
numPastedLines := len(i.pastedLines)
|
||||||
|
if numPastedLines > 0 {
|
||||||
|
output = strings.Join(i.pastedLines, "\n") + "\n" + output
|
||||||
|
i.pastedLines = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move cursor to the last display line of the current buffer
|
||||||
|
currLine := buf.DisplayPos / buf.LineWidth
|
||||||
|
lastLine := buf.DisplaySize() / buf.LineWidth
|
||||||
|
if lastLine > currLine {
|
||||||
|
fmt.Print(CursorDownN(lastLine - currLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all lines from bottom to top: buffer wrapped lines + pasted lines
|
||||||
|
for range lastLine + numPastedLines {
|
||||||
|
fmt.Print(CursorBOL + ClearToEOL + CursorUp)
|
||||||
|
}
|
||||||
|
fmt.Print(CursorBOL + ClearToEOL)
|
||||||
|
|
||||||
|
i.Prompt.UseAlt = false
|
||||||
|
return output, ErrEditPrompt
|
||||||
case CharCtrlZ:
|
case CharCtrlZ:
|
||||||
fd := os.Stdin.Fd()
|
fd := os.Stdin.Fd()
|
||||||
return handleCharCtrlZ(fd, i.Terminal.termios)
|
return handleCharCtrlZ(fd, i.Terminal.termios)
|
||||||
|
|||||||
Reference in New Issue
Block a user