From e6450ae499463fac102e49190f59d160d087a1fd Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 07:30:10 +0200 Subject: [PATCH 1/9] implemented open prompt in system text editor via Ctrl+X --- cmd/cli/commands/run.go | 1 + cmd/cli/readline/editor.go | 44 +++++++++++++++ cmd/cli/readline/editor_test.go | 84 ++++++++++++++++++++++++++++ cmd/cli/readline/readline.go | 6 ++ cmd/cli/readline/readline_unix.go | 20 +++++++ cmd/cli/readline/readline_windows.go | 20 +++++++ cmd/cli/readline/types.go | 1 + 7 files changed, 176 insertions(+) create mode 100644 cmd/cli/readline/editor.go create mode 100644 cmd/cli/readline/editor_test.go diff --git a/cmd/cli/commands/run.go b/cmd/cli/commands/run.go index b585c9a82..3fc089ee4 100644 --- a/cmd/cli/commands/run.go +++ b/cmd/cli/commands/run.go @@ -132,6 +132,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 default 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, "") diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go new file mode 100644 index 000000000..218897ff9 --- /dev/null +++ b/cmd/cli/readline/editor.go @@ -0,0 +1,44 @@ +package readline + +import ( + "os" + "os/exec" + "strings" +) + +func runEditor(content string, defaultEditor string) (string, error) { + tmpFile, err := os.CreateTemp("", "docker-model-prompt-*.txt") + if err != nil { + return content, err + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.Write([]byte(content)); err != nil { + tmpFile.Close() + return content, err + } + tmpFile.Close() + + editor := strings.TrimSpace(os.Getenv("EDITOR")) + if editor == "" { + editor = defaultEditor + } + + // handle for env varibles set with args + parts := strings.Fields(editor) + args := append(parts[1:], tmpFile.Name()) + cmd := exec.Command(parts[0], args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return content, err + } + + edited, err := os.ReadFile(tmpFile.Name()) + if err != nil { + return content, err + } + + return string(edited), nil +} diff --git a/cmd/cli/readline/editor_test.go b/cmd/cli/readline/editor_test.go new file mode 100644 index 000000000..abc6d46da --- /dev/null +++ b/cmd/cli/readline/editor_test.go @@ -0,0 +1,84 @@ +//go:build !windows + +package readline + +import ( + "os" + "path/filepath" + "testing" +) + +func createMockEditor(t *testing.T, scriptBody string) string { + t.Helper() + editorScript := filepath.Join(t.TempDir(), "mock-editor.sh") + if err := os.WriteFile(editorScript, []byte("#!/bin/sh\n"+scriptBody+"\n"), 0o755); err != nil { + t.Fatalf("failed to create mock editor: %v", err) + } + t.Setenv("EDITOR", editorScript) + return editorScript +} + +func TestRunEditor(t *testing.T) { + tests := []struct { + name string + mockEditorScript string + input string + expected string + }{ + { + name: "modifies content", + mockEditorScript: `printf " edited" >> "$1"`, + input: "hello docker model prompt", + expected: "hello docker model prompt edited", + }, + { + name: "empty content", + mockEditorScript: `printf "new content" > "$1"`, + input: "", + expected: "new content", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + createMockEditor(t, tt.mockEditorScript) + + result, err := runEditor(tt.input, "vi") + if err != nil { + t.Fatalf("runEditor failed: %v", err) + } + + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + }) + } +} + +func TestRunEditorReturnsOriginalContentOnFailure(t *testing.T) { + t.Setenv("EDITOR", "non_exists_editor") + + content := "docker model prompt hello" + result, err := runEditor(content, "vi") + if err == nil { + t.Fatal("expected error from nonexistent editor") + } + + if result != content { + t.Errorf("expected original content on failure, got %q", result) + } +} + +func TestRunEditorWithEditorArgs(t *testing.T) { + editorScript := createMockEditor(t, `printf "edited with args" > "$2"`) + t.Setenv("EDITOR", editorScript+" --wait") + + result, err := runEditor("original", "vi") + if err != nil { + t.Fatalf("runEditor failed: %v", err) + } + + if result != "edited with args" { + t.Errorf("expected %q, got %q", "edited with args", result) + } +} diff --git a/cmd/cli/readline/readline.go b/cmd/cli/readline/readline.go index d8121362e..23b36e8c1 100644 --- a/cmd/cli/readline/readline.go +++ b/cmd/cli/readline/readline.go @@ -209,6 +209,12 @@ func (i *Instance) Readline() (string, error) { buf.ClearScreen() case CharCtrlW: buf.DeleteWord() + case CharCtrlX: + fd := os.Stdin.Fd() + edited, err := openInEditor(fd, i.Terminal.termios, buf.String()) + if err == nil { + buf.Replace([]rune(edited)) + } case CharCtrlZ: fd := os.Stdin.Fd() return handleCharCtrlZ(fd, i.Terminal.termios) diff --git a/cmd/cli/readline/readline_unix.go b/cmd/cli/readline/readline_unix.go index d48b91769..95224b57d 100644 --- a/cmd/cli/readline/readline_unix.go +++ b/cmd/cli/readline/readline_unix.go @@ -17,3 +17,23 @@ func handleCharCtrlZ(fd uintptr, termios any) (string, error) { // on resume... return "", nil } + +func openInEditor(fd uintptr, termios any, content string) (string, error) { + t := termios.(*Termios) + + if err := UnsetRawMode(fd, t); err != nil { + return content, err + } + + edited, err := runEditor(content, "vi") + if err != nil { + SetRawMode(fd) + return content, err + } + + if _, err := SetRawMode(fd); err != nil { + return edited, err + } + + return edited, nil +} diff --git a/cmd/cli/readline/readline_windows.go b/cmd/cli/readline/readline_windows.go index a131d0ef7..5c08373aa 100644 --- a/cmd/cli/readline/readline_windows.go +++ b/cmd/cli/readline/readline_windows.go @@ -4,3 +4,23 @@ func handleCharCtrlZ(fd uintptr, state any) (string, error) { // not supported return "", nil } + +func openInEditor(fd uintptr, termios any, content string) (string, error) { + s := termios.(*State) + + if err := UnsetRawMode(fd, s); err != nil { + return content, err + } + + edited, err := runEditor(content, "notepad") + if err != nil { + SetRawMode(fd) + return content, err + } + + if _, err := SetRawMode(fd); err != nil { + return edited, err + } + + return edited, nil +} diff --git a/cmd/cli/readline/types.go b/cmd/cli/readline/types.go index f4efa8d92..a18b71d32 100644 --- a/cmd/cli/readline/types.go +++ b/cmd/cli/readline/types.go @@ -24,6 +24,7 @@ const ( CharTranspose = 20 CharCtrlU = 21 CharCtrlW = 23 + CharCtrlX = 24 CharCtrlY = 25 CharCtrlZ = 26 CharEsc = 27 From 67b02481bba92937b75ce01a01d26e6c9fdc6315 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 07:40:29 +0200 Subject: [PATCH 2/9] fix comment typo --- cmd/cli/readline/editor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go index 218897ff9..4228e1cc2 100644 --- a/cmd/cli/readline/editor.go +++ b/cmd/cli/readline/editor.go @@ -24,7 +24,7 @@ func runEditor(content string, defaultEditor string) (string, error) { editor = defaultEditor } - // handle for env varibles set with args + // handle for env variables set with args parts := strings.Fields(editor) args := append(parts[1:], tmpFile.Name()) cmd := exec.Command(parts[0], args...) From f3862748e548c9eb40f0cb57020d3ec35f371365 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 09:07:26 +0200 Subject: [PATCH 3/9] addressing review comments --- cmd/cli/commands/run.go | 2 +- cmd/cli/readline/editor.go | 34 ++++++++++++++++++---------- cmd/cli/readline/readline_unix.go | 12 ++++++---- cmd/cli/readline/readline_windows.go | 12 ++++++---- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/cmd/cli/commands/run.go b/cmd/cli/commands/run.go index 3fc089ee4..9335e3537 100644 --- a/cmd/cli/commands/run.go +++ b/cmd/cli/commands/run.go @@ -132,7 +132,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 default system text editor") + fmt.Fprintln(os.Stderr, " Ctrl + x Open prompt in text editor ($EDITOR)") fmt.Fprintln(os.Stderr, " Ctrl + c Stop the model from responding") fmt.Fprintln(os.Stderr, " Ctrl + d Exit (/bye)") fmt.Fprintln(os.Stderr, "") diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go index 4228e1cc2..db6d7f764 100644 --- a/cmd/cli/readline/editor.go +++ b/cmd/cli/readline/editor.go @@ -3,6 +3,7 @@ package readline import ( "os" "os/exec" + "runtime" "strings" ) @@ -19,18 +20,7 @@ func runEditor(content string, defaultEditor string) (string, error) { } tmpFile.Close() - editor := strings.TrimSpace(os.Getenv("EDITOR")) - if editor == "" { - editor = defaultEditor - } - - // handle for env variables set with args - parts := strings.Fields(editor) - args := append(parts[1:], tmpFile.Name()) - cmd := exec.Command(parts[0], args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr + cmd := buildEditorCmd(defaultEditor, tmpFile.Name()) if err := cmd.Run(); err != nil { return content, err } @@ -42,3 +32,23 @@ func runEditor(content string, defaultEditor string) (string, error) { return string(edited), nil } + +func buildEditorCmd(defaultEditor string, filePath string) *exec.Cmd { + editor := strings.TrimSpace(os.Getenv("EDITOR")) + if editor == "" { + editor = defaultEditor + } + + var cmd *exec.Cmd + if runtime.GOOS == "windows" { + parts := strings.Fields(editor) + args := append(parts[1:], filePath) + cmd = exec.Command(parts[0], args...) + } else { + cmd = exec.Command("sh", "-c", editor+" \"$1\"", "--", filePath) + } + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd +} diff --git a/cmd/cli/readline/readline_unix.go b/cmd/cli/readline/readline_unix.go index 95224b57d..0271da8d6 100644 --- a/cmd/cli/readline/readline_unix.go +++ b/cmd/cli/readline/readline_unix.go @@ -26,13 +26,15 @@ func openInEditor(fd uintptr, termios any, content string) (string, error) { } edited, err := runEditor(content, "vi") - if err != nil { - SetRawMode(fd) - return content, err + + // Always restore raw mode using the original termios, whether the editor + // succeeded or failed, so the terminal returns to its previous configuration. + if _, restoreErr := SetRawMode(fd); restoreErr != nil { + return content, restoreErr } - if _, err := SetRawMode(fd); err != nil { - return edited, err + if err != nil { + return content, err } return edited, nil diff --git a/cmd/cli/readline/readline_windows.go b/cmd/cli/readline/readline_windows.go index 5c08373aa..9243c1fca 100644 --- a/cmd/cli/readline/readline_windows.go +++ b/cmd/cli/readline/readline_windows.go @@ -13,13 +13,15 @@ func openInEditor(fd uintptr, termios any, content string) (string, error) { } edited, err := runEditor(content, "notepad") - if err != nil { - SetRawMode(fd) - return content, err + + // Always restore raw mode using the original state, whether the editor + // succeeded or failed, so the terminal returns to its previous configuration. + if _, restoreErr := SetRawMode(fd); restoreErr != nil { + return content, restoreErr } - if _, err := SetRawMode(fd); err != nil { - return edited, err + if err != nil { + return content, err } return edited, nil From c9b7b44dc96c03c7bfda67cbdc1d033fb8012795 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 09:25:34 +0200 Subject: [PATCH 4/9] fixing review comments --- cmd/cli/readline/editor.go | 8 ++++---- cmd/cli/readline/editor_test.go | 12 ++++++++++++ cmd/cli/readline/readline.go | 6 ++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go index db6d7f764..cd4bfb710 100644 --- a/cmd/cli/readline/editor.go +++ b/cmd/cli/readline/editor.go @@ -30,7 +30,9 @@ func runEditor(content string, defaultEditor string) (string, error) { return content, err } - return string(edited), nil + result := strings.TrimRight(string(edited), "\r\n") + + return result, nil } func buildEditorCmd(defaultEditor string, filePath string) *exec.Cmd { @@ -41,9 +43,7 @@ func buildEditorCmd(defaultEditor string, filePath string) *exec.Cmd { var cmd *exec.Cmd if runtime.GOOS == "windows" { - parts := strings.Fields(editor) - args := append(parts[1:], filePath) - cmd = exec.Command(parts[0], args...) + cmd = exec.Command("cmd", "/C", editor+" \""+filePath+"\"") } else { cmd = exec.Command("sh", "-c", editor+" \"$1\"", "--", filePath) } diff --git a/cmd/cli/readline/editor_test.go b/cmd/cli/readline/editor_test.go index abc6d46da..b5e3636c4 100644 --- a/cmd/cli/readline/editor_test.go +++ b/cmd/cli/readline/editor_test.go @@ -37,6 +37,18 @@ func TestRunEditor(t *testing.T) { input: "", expected: "new content", }, + { + name: "strips trailing newline", + mockEditorScript: `printf "edited\n" > "$1"`, + input: "", + expected: "edited", + }, + { + name: "strips trailing carriage return and newline", + mockEditorScript: `printf "edited\r\n" > "$1"`, + input: "", + expected: "edited", + }, } for _, tt := range tests { diff --git a/cmd/cli/readline/readline.go b/cmd/cli/readline/readline.go index 23b36e8c1..66f315691 100644 --- a/cmd/cli/readline/readline.go +++ b/cmd/cli/readline/readline.go @@ -212,9 +212,11 @@ func (i *Instance) Readline() (string, error) { case CharCtrlX: fd := os.Stdin.Fd() edited, err := openInEditor(fd, i.Terminal.termios, buf.String()) - if err == nil { - buf.Replace([]rune(edited)) + if err != nil { + fmt.Fprintf(os.Stderr, "error opening editor: %s\n", err) + break } + buf.Replace([]rune(edited)) case CharCtrlZ: fd := os.Stdin.Fd() return handleCharCtrlZ(fd, i.Terminal.termios) From c0fc720e9993dd254b0539c962115022ebca14e2 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 10:11:56 +0200 Subject: [PATCH 5/9] fixed review comments --- cmd/cli/readline/editor.go | 89 +++++++++++++++++++++------- cmd/cli/readline/editor_test.go | 6 +- cmd/cli/readline/readline_unix.go | 4 +- cmd/cli/readline/readline_windows.go | 4 +- 4 files changed, 71 insertions(+), 32 deletions(-) diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go index cd4bfb710..3d46e8a5c 100644 --- a/cmd/cli/readline/editor.go +++ b/cmd/cli/readline/editor.go @@ -1,13 +1,76 @@ package readline import ( + "fmt" "os" "os/exec" "runtime" "strings" ) -func runEditor(content string, defaultEditor string) (string, error) { +const ( + defaultEditor = "vi" + defaultShell = "/bin/bash" + windowsEditor = "notepad" + windowsShell = "cmd" +) + +func platformize(linux, windows string) string { + if runtime.GOOS == "windows" { + return windows + } + return linux +} + +func defaultEnvShell() []string { + shell := os.Getenv("SHELL") + if len(shell) == 0 { + shell = platformize(defaultShell, windowsShell) + } + flag := "-c" + if shell == windowsShell { + flag = "/C" + } + return []string{shell, flag} +} + +func resolveEditor() ([]string, bool) { + editor := strings.TrimSpace(os.Getenv("EDITOR")) + if len(editor) == 0 { + editor = platformize(defaultEditor, windowsEditor) + } + + if !strings.Contains(editor, " ") { + return []string{editor}, false + } + + if !strings.ContainsAny(editor, "\"'\\") { + return strings.Split(editor, " "), false + } + + shell := defaultEnvShell() + return append(shell, editor), true +} + +func buildEditorCmd(filePath string) *exec.Cmd { + args, shell := resolveEditor() + + if shell { + // The editor string is the last element — append the file path to it + // so the shell interprets the full command. + args[len(args)-1] = fmt.Sprintf("%s %s", args[len(args)-1], filePath) + } else { + args = append(args, filePath) + } + + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd +} + +func runEditor(content string) (string, error) { tmpFile, err := os.CreateTemp("", "docker-model-prompt-*.txt") if err != nil { return content, err @@ -20,7 +83,7 @@ func runEditor(content string, defaultEditor string) (string, error) { } tmpFile.Close() - cmd := buildEditorCmd(defaultEditor, tmpFile.Name()) + cmd := buildEditorCmd(tmpFile.Name()) if err := cmd.Run(); err != nil { return content, err } @@ -30,25 +93,5 @@ func runEditor(content string, defaultEditor string) (string, error) { return content, err } - result := strings.TrimRight(string(edited), "\r\n") - - return result, nil -} - -func buildEditorCmd(defaultEditor string, filePath string) *exec.Cmd { - editor := strings.TrimSpace(os.Getenv("EDITOR")) - if editor == "" { - editor = defaultEditor - } - - var cmd *exec.Cmd - if runtime.GOOS == "windows" { - cmd = exec.Command("cmd", "/C", editor+" \""+filePath+"\"") - } else { - cmd = exec.Command("sh", "-c", editor+" \"$1\"", "--", filePath) - } - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd + return strings.TrimRight(string(edited), "\r\n"), nil } diff --git a/cmd/cli/readline/editor_test.go b/cmd/cli/readline/editor_test.go index b5e3636c4..64489d813 100644 --- a/cmd/cli/readline/editor_test.go +++ b/cmd/cli/readline/editor_test.go @@ -55,7 +55,7 @@ func TestRunEditor(t *testing.T) { t.Run(tt.name, func(t *testing.T) { createMockEditor(t, tt.mockEditorScript) - result, err := runEditor(tt.input, "vi") + result, err := runEditor(tt.input) if err != nil { t.Fatalf("runEditor failed: %v", err) } @@ -71,7 +71,7 @@ func TestRunEditorReturnsOriginalContentOnFailure(t *testing.T) { t.Setenv("EDITOR", "non_exists_editor") content := "docker model prompt hello" - result, err := runEditor(content, "vi") + result, err := runEditor(content) if err == nil { t.Fatal("expected error from nonexistent editor") } @@ -85,7 +85,7 @@ func TestRunEditorWithEditorArgs(t *testing.T) { editorScript := createMockEditor(t, `printf "edited with args" > "$2"`) t.Setenv("EDITOR", editorScript+" --wait") - result, err := runEditor("original", "vi") + result, err := runEditor("original") if err != nil { t.Fatalf("runEditor failed: %v", err) } diff --git a/cmd/cli/readline/readline_unix.go b/cmd/cli/readline/readline_unix.go index 0271da8d6..05f8515fc 100644 --- a/cmd/cli/readline/readline_unix.go +++ b/cmd/cli/readline/readline_unix.go @@ -25,10 +25,8 @@ func openInEditor(fd uintptr, termios any, content string) (string, error) { return content, err } - edited, err := runEditor(content, "vi") + edited, err := runEditor(content) - // Always restore raw mode using the original termios, whether the editor - // succeeded or failed, so the terminal returns to its previous configuration. if _, restoreErr := SetRawMode(fd); restoreErr != nil { return content, restoreErr } diff --git a/cmd/cli/readline/readline_windows.go b/cmd/cli/readline/readline_windows.go index 9243c1fca..b06d4b641 100644 --- a/cmd/cli/readline/readline_windows.go +++ b/cmd/cli/readline/readline_windows.go @@ -12,10 +12,8 @@ func openInEditor(fd uintptr, termios any, content string) (string, error) { return content, err } - edited, err := runEditor(content, "notepad") + edited, err := runEditor(content) - // Always restore raw mode using the original state, whether the editor - // succeeded or failed, so the terminal returns to its previous configuration. if _, restoreErr := SetRawMode(fd); restoreErr != nil { return content, restoreErr } From 85ad3a85292f9a8486c5b9072070e3ab222b1bea Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 10:25:58 +0200 Subject: [PATCH 6/9] Fixed review comments and changed the usage doc --- cmd/cli/commands/run.go | 2 +- cmd/cli/readline/editor.go | 3 +-- cmd/cli/readline/readline_unix.go | 3 ++- cmd/cli/readline/readline_windows.go | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/cli/commands/run.go b/cmd/cli/commands/run.go index 9335e3537..7ec76d887 100644 --- a/cmd/cli/commands/run.go +++ b/cmd/cli/commands/run.go @@ -132,7 +132,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 text editor ($EDITOR)") + fmt.Fprintln(os.Stderr, " Ctrl + x Open prompt in your default 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, "") diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go index 3d46e8a5c..5f35f26e6 100644 --- a/cmd/cli/readline/editor.go +++ b/cmd/cli/readline/editor.go @@ -57,8 +57,7 @@ func buildEditorCmd(filePath string) *exec.Cmd { if shell { // The editor string is the last element — append the file path to it - // so the shell interprets the full command. - args[len(args)-1] = fmt.Sprintf("%s %s", args[len(args)-1], filePath) + args[len(args)-1] = fmt.Sprintf("%s '%s'", args[len(args)-1], filePath) } else { args = append(args, filePath) } diff --git a/cmd/cli/readline/readline_unix.go b/cmd/cli/readline/readline_unix.go index 05f8515fc..9bd69e3c7 100644 --- a/cmd/cli/readline/readline_unix.go +++ b/cmd/cli/readline/readline_unix.go @@ -3,6 +3,7 @@ package readline import ( + "errors" "syscall" ) @@ -28,7 +29,7 @@ func openInEditor(fd uintptr, termios any, content string) (string, error) { edited, err := runEditor(content) if _, restoreErr := SetRawMode(fd); restoreErr != nil { - return content, restoreErr + return content, errors.Join(err, restoreErr) } if err != nil { diff --git a/cmd/cli/readline/readline_windows.go b/cmd/cli/readline/readline_windows.go index b06d4b641..f2ff9a1b4 100644 --- a/cmd/cli/readline/readline_windows.go +++ b/cmd/cli/readline/readline_windows.go @@ -1,5 +1,7 @@ package readline +import "errors" + func handleCharCtrlZ(fd uintptr, state any) (string, error) { // not supported return "", nil @@ -15,7 +17,7 @@ func openInEditor(fd uintptr, termios any, content string) (string, error) { edited, err := runEditor(content) if _, restoreErr := SetRawMode(fd); restoreErr != nil { - return content, restoreErr + return content, errors.Join(err, restoreErr) } if err != nil { From 3e43c51e813ce78eb3aea589f5d29f5aa81a521a Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 10:37:55 +0200 Subject: [PATCH 7/9] chnage to use /bin/sh as default shell for more portability --- cmd/cli/readline/editor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go index 5f35f26e6..3b1a8eeb1 100644 --- a/cmd/cli/readline/editor.go +++ b/cmd/cli/readline/editor.go @@ -10,7 +10,7 @@ import ( const ( defaultEditor = "vi" - defaultShell = "/bin/bash" + defaultShell = "/bin/sh" windowsEditor = "notepad" windowsShell = "cmd" ) From 19289cce90f2be30f7fb248bc2a4b496b8f67006 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 13:27:29 +0200 Subject: [PATCH 8/9] refactored code for review comments trying to resolve lint issues --- cmd/cli/readline/editor.go | 30 +++++++++++++++++++++++----- cmd/cli/readline/readline_unix.go | 21 ------------------- cmd/cli/readline/readline_windows.go | 22 -------------------- 3 files changed, 25 insertions(+), 48 deletions(-) diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go index 3b1a8eeb1..039c36b3f 100644 --- a/cmd/cli/readline/editor.go +++ b/cmd/cli/readline/editor.go @@ -1,6 +1,7 @@ package readline import ( + "errors" "fmt" "os" "os/exec" @@ -15,6 +16,24 @@ const ( windowsShell = "cmd" ) +func openInEditor(fd uintptr, termios any, content string) (string, error) { + if err := UnsetRawMode(fd, termios); err != nil { + return content, err + } + + edited, err := runEditor(content) + + if _, restoreErr := SetRawMode(fd); restoreErr != nil { + return content, errors.Join(err, restoreErr) + } + + if err != nil { + return content, err + } + + return edited, nil +} + func platformize(linux, windows string) string { if runtime.GOOS == "windows" { return windows @@ -24,7 +43,7 @@ func platformize(linux, windows string) string { func defaultEnvShell() []string { shell := os.Getenv("SHELL") - if len(shell) == 0 { + if shell == "" { shell = platformize(defaultShell, windowsShell) } flag := "-c" @@ -36,7 +55,7 @@ func defaultEnvShell() []string { func resolveEditor() ([]string, bool) { editor := strings.TrimSpace(os.Getenv("EDITOR")) - if len(editor) == 0 { + if editor == "" { editor = platformize(defaultEditor, windowsEditor) } @@ -57,12 +76,13 @@ func buildEditorCmd(filePath string) *exec.Cmd { if shell { // The editor string is the last element — append the file path to it - args[len(args)-1] = fmt.Sprintf("%s '%s'", args[len(args)-1], filePath) + safeFilePath := strings.ReplaceAll(filePath, "'", "'\\''") + args[len(args)-1] = fmt.Sprintf("%s '%s'", args[len(args)-1], safeFilePath) } else { args = append(args, filePath) } - cmd := exec.Command(args[0], args[1:]...) + cmd := exec.Command(args[0], args[1:]...) //nolint:gosec // $EDITOR is a user-controlled local env var, same trust model as git/kubectl cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -76,7 +96,7 @@ func runEditor(content string) (string, error) { } defer os.Remove(tmpFile.Name()) - if _, err := tmpFile.Write([]byte(content)); err != nil { + if _, err := tmpFile.WriteString(content); err != nil { tmpFile.Close() return content, err } diff --git a/cmd/cli/readline/readline_unix.go b/cmd/cli/readline/readline_unix.go index 9bd69e3c7..d48b91769 100644 --- a/cmd/cli/readline/readline_unix.go +++ b/cmd/cli/readline/readline_unix.go @@ -3,7 +3,6 @@ package readline import ( - "errors" "syscall" ) @@ -18,23 +17,3 @@ func handleCharCtrlZ(fd uintptr, termios any) (string, error) { // on resume... return "", nil } - -func openInEditor(fd uintptr, termios any, content string) (string, error) { - t := termios.(*Termios) - - if err := UnsetRawMode(fd, t); err != nil { - return content, err - } - - edited, err := runEditor(content) - - if _, restoreErr := SetRawMode(fd); restoreErr != nil { - return content, errors.Join(err, restoreErr) - } - - if err != nil { - return content, err - } - - return edited, nil -} diff --git a/cmd/cli/readline/readline_windows.go b/cmd/cli/readline/readline_windows.go index f2ff9a1b4..a131d0ef7 100644 --- a/cmd/cli/readline/readline_windows.go +++ b/cmd/cli/readline/readline_windows.go @@ -1,28 +1,6 @@ package readline -import "errors" - func handleCharCtrlZ(fd uintptr, state any) (string, error) { // not supported return "", nil } - -func openInEditor(fd uintptr, termios any, content string) (string, error) { - s := termios.(*State) - - if err := UnsetRawMode(fd, s); err != nil { - return content, err - } - - edited, err := runEditor(content) - - if _, restoreErr := SetRawMode(fd); restoreErr != nil { - return content, errors.Join(err, restoreErr) - } - - if err != nil { - return content, err - } - - return edited, nil -} From ff4f1d70fe7f7c942c9cbe4d42c2bd6eab374347 Mon Sep 17 00:00:00 2001 From: sathiraumesh Date: Wed, 1 Apr 2026 13:45:02 +0200 Subject: [PATCH 9/9] refactored the nolint spec --- cmd/cli/readline/editor.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/cli/readline/editor.go b/cmd/cli/readline/editor.go index 039c36b3f..ee20340f7 100644 --- a/cmd/cli/readline/editor.go +++ b/cmd/cli/readline/editor.go @@ -82,7 +82,8 @@ func buildEditorCmd(filePath string) *exec.Cmd { args = append(args, filePath) } - cmd := exec.Command(args[0], args[1:]...) //nolint:gosec // $EDITOR is a user-controlled local env var, same trust model as git/kubectl + //nolint:gosec // $EDITOR is a user-controlled local env var, same trust model as git/kubectl + cmd := exec.Command(args[0], args[1:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr