Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/cli/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
model-cli
model-cli.exe
.idea/
dist/
1 change: 1 addition & 0 deletions cmd/cli/commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func generateInteractiveWithReadline(cmd *cobra.Command, desktopClient *desktop.
fmt.Fprintln(os.Stderr, " Ctrl + w Delete the word before the cursor")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, " Ctrl + l Clear the screen")
fmt.Fprintln(os.Stderr, " Ctrl + x Open prompt in system text editor")
fmt.Fprintln(os.Stderr, " Ctrl + c Stop the model from responding")
fmt.Fprintln(os.Stderr, " Ctrl + d Exit (/bye)")
fmt.Fprintln(os.Stderr, "")
Expand Down
86 changes: 86 additions & 0 deletions cmd/cli/readline/editor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//go:build !windows

package readline

import (
"fmt"
"os"
"os/exec"
"strings"
)

// getEditor returns the preferred text editor from environment variables.
// It checks VISUAL first, then EDITOR, and falls back to "vi" as default.
func getEditor() string {
if editor := os.Getenv("VISUAL"); editor != "" {
return editor
}
if editor := os.Getenv("EDITOR"); editor != "" {
return editor
}
return "vi"
}

// OpenInEditor opens a temporary file with the given content in the user's
// preferred text editor. It returns the edited content after the editor closes.
// The function handles restoring terminal mode before launching the editor
// and setting it back to raw mode after the editor closes.
func OpenInEditor(fd uintptr, termios any, currentContent string) (string, error) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "model-prompt-*.txt")
if err != nil {
return "", err
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)

// Write current content to the file
if _, err := tmpFile.WriteString(currentContent); err != nil {
tmpFile.Close()
return "", err
}
if err := tmpFile.Close(); err != nil {
return "", err
}

// Restore terminal to normal mode before launching editor
t, ok := termios.(*Termios)
if !ok {
return "", fmt.Errorf("invalid termios type")
}
if err := UnsetRawMode(fd, t); err != nil {
return "", err
}

// Get the editor and launch it
editor := getEditor()
editorParts := strings.Fields(editor)
editorCmd := editorParts[0]
editorArgs := append(editorParts[1:], tmpPath)

cmd := exec.Command(editorCmd, editorArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
// Try to restore raw mode even if editor failed
_, _ = SetRawMode(fd)
return "", err
}

// Restore raw mode after editor closes
if _, err := SetRawMode(fd); err != nil {
return "", err
}

// Read the edited content
content, err := os.ReadFile(tmpPath)
if err != nil {
return "", err
}

// Trim trailing newlines that editors typically add
result := strings.TrimSuffix(string(content), "\n")
return result, nil
}
83 changes: 83 additions & 0 deletions cmd/cli/readline/editor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//go:build !windows

package readline

import (
"os"
"testing"
)

func TestGetEditor(t *testing.T) {
tests := []struct {
name string
visual string
editor string
expected string
}{
{
name: "VISUAL environment variable set",
visual: "nano",
editor: "",
expected: "nano",
},
{
name: "EDITOR environment variable set",
visual: "",
editor: "emacs",
expected: "emacs",
},
{
name: "VISUAL takes precedence over EDITOR",
visual: "code",
editor: "nano",
expected: "code",
},
{
name: "default to vi when no environment variables set",
visual: "",
editor: "",
expected: "vi",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save original values
origVisual := os.Getenv("VISUAL")
origEditor := os.Getenv("EDITOR")

// Set test values
if tt.visual != "" {
os.Setenv("VISUAL", tt.visual)
} else {
os.Unsetenv("VISUAL")
}

if tt.editor != "" {
os.Setenv("EDITOR", tt.editor)
} else {
os.Unsetenv("EDITOR")
}

// Run test
result := getEditor()

// Restore original values
if origVisual != "" {
os.Setenv("VISUAL", origVisual)
} else {
os.Unsetenv("VISUAL")
}

if origEditor != "" {
os.Setenv("EDITOR", origEditor)
} else {
os.Unsetenv("EDITOR")
}

if result != tt.expected {
t.Errorf("getEditor() = %q, want %q", result, tt.expected)
}
})
}
}
86 changes: 86 additions & 0 deletions cmd/cli/readline/editor_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//go:build windows

package readline

import (
"fmt"
"os"
"os/exec"
"strings"
)

// getEditor returns the preferred text editor from environment variables.
// It checks VISUAL first, then EDITOR, and falls back to "notepad" as default on Windows.
func getEditor() string {
if editor := os.Getenv("VISUAL"); editor != "" {
return editor
}
if editor := os.Getenv("EDITOR"); editor != "" {
return editor
}
return "notepad"
}

// OpenInEditor opens a temporary file with the given content in the user's
// preferred text editor. It returns the edited content after the editor closes.
// The function handles restoring terminal mode before launching the editor
// and setting it back to raw mode after the editor closes.
func OpenInEditor(fd uintptr, termios any, currentContent string) (string, error) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "model-prompt-*.txt")
if err != nil {
return "", err
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)

// Write current content to the file
if _, err := tmpFile.WriteString(currentContent); err != nil {
tmpFile.Close()
return "", err
}
if err := tmpFile.Close(); err != nil {
return "", err
}

// Restore terminal to normal mode before launching editor
s, ok := termios.(*State)
if !ok {
return "", fmt.Errorf("invalid state type")
}
if err := UnsetRawMode(fd, s); err != nil {
return "", err
}

// Get the editor and launch it
editor := getEditor()
editorParts := strings.Fields(editor)
editorCmd := editorParts[0]
editorArgs := append(editorParts[1:], tmpPath)

cmd := exec.Command(editorCmd, editorArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Run(); err != nil {
// Try to restore raw mode even if editor failed
_, _ = SetRawMode(fd)
return "", err
}

// Restore raw mode after editor closes
if _, err := SetRawMode(fd); err != nil {
return "", err
}

// Read the edited content
content, err := os.ReadFile(tmpPath)
if err != nil {
return "", err
}

// Trim trailing newlines that editors typically add
result := strings.TrimSuffix(string(content), "\n")
return result, nil
}
12 changes: 12 additions & 0 deletions cmd/cli/readline/readline.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,18 @@ func (i *Instance) Readline() (string, error) {
buf.ClearScreen()
case CharCtrlW:
buf.DeleteWord()
case CharCtrlX:
fd := os.Stdin.Fd()
content, err := OpenInEditor(fd, i.Terminal.termios, buf.String())
if err != nil {
// If editor fails, just continue with existing content
fmt.Print(ClearScreen + CursorReset + i.Prompt.prompt())
buf.ClearScreen()
} else {
// Replace buffer with edited content
fmt.Print(ClearScreen + CursorReset + i.Prompt.prompt())
buf.Replace([]rune(content))
}
case CharCtrlZ:
fd := os.Stdin.Fd()
return handleCharCtrlZ(fd, i.Terminal.termios)
Expand Down
1 change: 1 addition & 0 deletions cmd/cli/readline/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
CharTranspose = 20
CharCtrlU = 21
CharCtrlW = 23
CharCtrlX = 24
CharCtrlY = 25
CharCtrlZ = 26
CharEsc = 27
Expand Down
15 changes: 7 additions & 8 deletions pkg/go-containerregistry/cmd/krane/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ replace github.com/google/go-containerregistry => ../../
require (
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.11.0
github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589
github.com/google/go-containerregistry v0.20.3
github.com/docker/model-runner/pkg/go-containerregistry v0.0.0-20251203142437-40446829248e
)

require (
Expand Down Expand Up @@ -38,7 +38,7 @@ require (
github.com/aws/smithy-go v1.23.2 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/docker/cli v28.2.2+incompatible // indirect
github.com/docker/cli v28.3.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.4 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
Expand All @@ -50,12 +50,11 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
gotest.tools/v3 v3.1.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)
Loading