-
Notifications
You must be signed in to change notification settings - Fork 5.8k
prompt: swallow escape sequences, both backspaces, exit 130 on Ctrl+C #14214
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,11 @@ import ( | |
|
|
||
| //go:generate mockgen -destination=./prompt_mock.go -self_package "github.com/docker/compose/v5/pkg/prompt" -package=prompt . UI | ||
|
|
||
| var errInterrupt = errors.New("interrupt") | ||
| // ErrInterrupt is returned by an interactive prompt when the user presses | ||
| // Ctrl+C. The terminal being in raw mode, no SIGINT is ever delivered: this | ||
| // error is the only interrupt signal callers get, and the command runner | ||
| // maps it to the conventional 130 exit status like a real SIGINT. | ||
| var ErrInterrupt = errors.New("interrupt") | ||
|
Comment on lines
-34
to
+38
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When looking at the original PR, I was actually considering if we wanted to have context-cancellation somewhere. (I kept the docker system prun
...
Are you sure you want to continue? [y/N] ^C |
||
|
|
||
| // UI - prompt user input | ||
| type UI interface { | ||
|
|
@@ -59,13 +63,8 @@ func (u User) Confirm(message string, defaultValue bool) (bool, error) { | |
| } | ||
| defer u.stdin.RestoreTerminal() | ||
|
|
||
| prompt := " [y/N]: " | ||
| if defaultValue { | ||
| prompt = " [Y/n]: " | ||
| } | ||
|
|
||
| for { | ||
| _, _ = fmt.Fprint(u.stdout, message+prompt) | ||
| _, _ = fmt.Fprint(u.stdout, message+confirmHint(defaultValue)) | ||
|
Comment on lines
-68
to
+67
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting that it added a helper (thought from your Slack thread that it generally preferred just raw inline)? |
||
|
|
||
| answer, err := readLine(u.reader, u.stdout) | ||
| if err != nil { | ||
|
|
@@ -83,6 +82,15 @@ func (u User) Confirm(message string, defaultValue bool) (bool, error) { | |
| } | ||
| } | ||
|
|
||
| // confirmHint renders the answer hint appended to every confirmation | ||
| // message, the capitalized letter marking the default. | ||
| func confirmHint(defaultValue bool) string { | ||
| if defaultValue { | ||
| return " [Y/n]: " | ||
| } | ||
| return " [y/N]: " | ||
| } | ||
|
|
||
| func readLine(in io.RuneReader, out io.Writer) (string, error) { | ||
| var line []rune | ||
|
|
||
|
|
@@ -95,7 +103,7 @@ func readLine(in io.RuneReader, out io.Writer) (string, error) { | |
| switch ch { | ||
| case 3: // Ctrl+C | ||
| _, _ = fmt.Fprint(out, "\r\n") | ||
| return "", errInterrupt | ||
| return "", ErrInterrupt | ||
|
|
||
| case 4: // Ctrl+D | ||
| return "", io.EOF | ||
|
|
@@ -104,12 +112,19 @@ func readLine(in io.RuneReader, out io.Writer) (string, error) { | |
| _, _ = fmt.Fprint(out, "\r\n") | ||
| return string(line), nil | ||
|
|
||
| case 127: // Backspace | ||
| case 8, 127: // Backspace (^H on some terminals, DEL on most) | ||
| if len(line) > 0 { | ||
| line = line[:len(line)-1] | ||
| _, _ = fmt.Fprint(out, "\b \b") | ||
| } | ||
|
|
||
| case 27: // ESC: swallow the whole escape sequence (arrow keys, F-keys) | ||
| // so its printable tail ("[A"...) is neither echoed nor taken as | ||
| // input — the raw terminal delivers those as bytes, not events | ||
| if err := discardEscapeSequence(in); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| default: | ||
| if unicode.IsControl(ch) { | ||
| continue | ||
|
|
@@ -120,6 +135,35 @@ func readLine(in io.RuneReader, out io.Writer) (string, error) { | |
| } | ||
| } | ||
|
|
||
| // discardEscapeSequence consumes the remainder of an ANSI escape sequence | ||
| // whose ESC has just been read: a CSI sequence ("ESC [", parameters, one | ||
| // final byte in 0x40-0x7E) or an SS3 one ("ESC O", one byte). A bare ESC | ||
| // followed by anything else swallows that single rune, close enough for a | ||
| // yes/no prompt. | ||
| func discardEscapeSequence(in io.RuneReader) error { | ||
| ch, _, err := in.ReadRune() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| switch ch { | ||
| case '[': // CSI: parameter/intermediate bytes then one final byte | ||
| for { | ||
| ch, _, err = in.ReadRune() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if ch >= 0x40 && ch <= 0x7E { | ||
| return nil | ||
| } | ||
| } | ||
| case 'O': // SS3 (application-mode cursor/function keys): one byte | ||
| _, _, err = in.ReadRune() | ||
| return err | ||
| default: | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // Pipe - aggregates prompt methods | ||
| type Pipe struct { | ||
| stdout io.Writer | ||
|
|
@@ -128,7 +172,9 @@ type Pipe struct { | |
|
|
||
| // Confirm asks for yes or no input | ||
| func (u Pipe) Confirm(message string, defaultValue bool) (bool, error) { | ||
| _, _ = fmt.Fprint(u.stdout, message) | ||
| // same hint as the interactive prompt: the message reaching a log or a | ||
| // piped consumer documents what was asked and what the default was | ||
| _, _ = fmt.Fprint(u.stdout, message+confirmHint(defaultValue)) | ||
| var answer string | ||
| _, _ = fmt.Fscanln(u.stdin, &answer) | ||
| return utils.StringToBool(answer), nil | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait; did I mess this up? (if so; why didn't CI fail?)