-
Notifications
You must be signed in to change notification settings - Fork 113
Implement open prompt in system text editor via Ctrl+X #818
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
doringeman
merged 9 commits into
docker:main
from
sathiraumesh:implement-open-prompt-in-editor
Apr 2, 2026
+223
−0
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e6450ae
implemented open prompt in system text editor via Ctrl+X
sathiraumesh 67b0248
fix comment typo
sathiraumesh f386274
addressing review comments
sathiraumesh c9b7b44
fixing review comments
sathiraumesh c0fc720
fixed review comments
sathiraumesh 85ad3a8
Fixed review comments and changed the usage doc
sathiraumesh 3e43c51
chnage to use /bin/sh as default shell for more portability
sathiraumesh 19289cc
refactored code for review comments trying to resolve lint issues
sathiraumesh ff4f1d7
refactored the nolint spec
sathiraumesh File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| package readline | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "runtime" | ||
| "strings" | ||
| ) | ||
|
|
||
| const ( | ||
| defaultEditor = "vi" | ||
| defaultShell = "/bin/sh" | ||
| windowsEditor = "notepad" | ||
| 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 | ||
| } | ||
| return linux | ||
| } | ||
|
|
||
| func defaultEnvShell() []string { | ||
| shell := os.Getenv("SHELL") | ||
| if shell == "" { | ||
| 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 editor == "" { | ||
| 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 | ||
| safeFilePath := strings.ReplaceAll(filePath, "'", "'\\''") | ||
| args[len(args)-1] = fmt.Sprintf("%s '%s'", args[len(args)-1], safeFilePath) | ||
| } else { | ||
| args = append(args, filePath) | ||
| } | ||
|
|
||
| //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 | ||
| return cmd | ||
| } | ||
|
|
||
| func runEditor(content 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.WriteString(content); err != nil { | ||
| tmpFile.Close() | ||
| return content, err | ||
| } | ||
| tmpFile.Close() | ||
|
|
||
| cmd := buildEditorCmd(tmpFile.Name()) | ||
| if err := cmd.Run(); err != nil { | ||
| return content, err | ||
| } | ||
|
|
||
| edited, err := os.ReadFile(tmpFile.Name()) | ||
| if err != nil { | ||
| return content, err | ||
| } | ||
|
|
||
| return strings.TrimRight(string(edited), "\r\n"), nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| //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", | ||
| }, | ||
| { | ||
| 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 { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| createMockEditor(t, tt.mockEditorScript) | ||
|
|
||
| result, err := runEditor(tt.input) | ||
| 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) | ||
| 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") | ||
| if err != nil { | ||
| t.Fatalf("runEditor failed: %v", err) | ||
| } | ||
|
|
||
| if result != "edited with args" { | ||
| t.Errorf("expected %q, got %q", "edited with args", result) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.