diff --git a/emacs.go b/emacs.go index d1b8936c..a7d38f57 100644 --- a/emacs.go +++ b/emacs.go @@ -476,16 +476,27 @@ func (rl *Shell) bracketedPasteBegin() { } if len(sequence) > 6 { - pasted := string(sequence[:len(sequence)-6]) - // Terminals send \r (or \r\n) for line breaks inside a bracketed paste. - // Normalise them to \n, otherwise the stray carriage returns corrupt the - // line buffer and break multiline display and evaluation. - pasted = strings.ReplaceAll(pasted, "\r\n", "\n") - pasted = strings.ReplaceAll(pasted, "\r", "\n") - rl.cursor.InsertAt([]rune(pasted)...) + rl.insertPastedText(string(sequence[:len(sequence)-6])) } } +func (rl *Shell) insertPastedText(text string) { + // Terminals send \r (or \r\n) for line breaks inside a bracketed paste. + // Normalise them to \n, otherwise the stray carriage returns corrupt the + // line buffer and break multiline display and evaluation. + text = strings.ReplaceAll(text, "\r\n", "\n") + text = strings.ReplaceAll(text, "\r", "\n") + + if rl.PasteTransformer != nil { + text = rl.PasteTransformer(text) + } + if text == "" { + return + } + + rl.cursor.InsertAt([]rune(text)...) +} + // skipCsiSequence consumes the remainder of a CSI escape sequence (the GNU // readline skip-csi-sequence command). Terminals encode many special keys as // "ESC [" followed by parameter/intermediate bytes (0x20-0x3F) and a single diff --git a/emacs_test.go b/emacs_test.go index 95a836bc..6653239d 100644 --- a/emacs_test.go +++ b/emacs_test.go @@ -12,12 +12,19 @@ import ( func feedPaste(t *testing.T, payload string) string { t.Helper() + return feedPasteWithTransformer(t, payload, nil) +} + +func feedPasteWithTransformer(t *testing.T, payload string, transformer func(string) string) string { + t.Helper() + line := new(core.Line) rl := &Shell{ Keys: new(core.Keys), line: line, cursor: core.NewCursor(line), } + rl.SetPasteTransformer(transformer) // The handler consumes keys until it sees the paste-end sequence, so the // terminator must be fed too — otherwise it would block waiting for input. @@ -52,6 +59,30 @@ func TestBracketedPasteNormalizesCarriageReturns(t *testing.T) { } } +func TestBracketedPasteTransformer(t *testing.T) { + got := feedPasteWithTransformer(t, "a\r\nb", func(text string) string { + if text != "a\nb" { + t.Fatalf("transformer saw %q, want normalized text", text) + } + + return "rewritten" + }) + + if got != "rewritten" { + t.Fatalf("transformed paste = %q, want rewritten", got) + } +} + +func TestBracketedPasteTransformerCanDropText(t *testing.T) { + got := feedPasteWithTransformer(t, "secret", func(string) string { + return "" + }) + + if got != "" { + t.Fatalf("dropped paste = %q, want empty line", got) + } +} + // drainKeys pops everything remaining in the key buffer and returns it. func drainKeys(rl *Shell) string { var b []byte diff --git a/shell.go b/shell.go index 655dbb85..0fa878f0 100644 --- a/shell.go +++ b/shell.go @@ -57,6 +57,10 @@ type Shell struct { // Once enabled, set to nil to disable again. SyntaxHighlighter func(line []rune) string + // PasteTransformer, when set, rewrites bracketed paste payloads before + // they are inserted into the input buffer. + PasteTransformer func(text string) string + // Completer is a function that produces completions. // It takes the readline line ([]rune) and cursor pos as parameters, // and returns completions with their associated metadata/settings. @@ -116,6 +120,16 @@ func NewShell(opts ...inputrc.Option) *Shell { return shell } +// SetPasteTransformer sets a function used to rewrite bracketed paste payloads +// before they are inserted into the input buffer. Passing nil disables it. +func (rl *Shell) SetPasteTransformer(fn func(text string) string) { + if rl == nil { + return + } + + rl.PasteTransformer = fn +} + // Line is the shell input line buffer. // Contains methods to search and modify its contents, // split itself with tokenizers, and displaying itself.