Add support for bracketed paste mode - #254
Conversation
This patch adds support for bracketed paste mode to yash. Bracketed paste mode is a set of special escape sequences, which are employed by many terminal emulators to allow programs run inside of them to distinguish pasted text from typed-in text [0]. This is useful for preventing pasted text from accidentally executing commands in the application it was pasted to. Commonly this problem arises when copying text from a web browser to a shell since the user may have copied hidden text from the web page which may contain control characters [1]. Bracketed paste mode is supported by all mainstream terminal emulators. However, the feature must be explicitly enabled through a newly added option (le-bracket-paste). By setting this option, the terminal emulator is instructed to emit the start/stop sequence once the user starts pasting text. Disabling the option again configures the terminal emulator to no longer emit these start/stop sequences. Currently, bracketed paste mode is only enabled in Vi insert mode and emacs mode, we could consider enabling it an additional modes in the future (e.g., the search mode). [0]: https://www.xfree86.org/4.7.0/ctlseqs.html#Bracketed%20Paste%20Mode [1]: https://thejh.net/misc/website-terminal-copy-paste
| if (le_bracketed_paste && tg.value.cmdfunc != cmd_bracketed_paste_end) { | ||
| assert(reader_second_buffer.length > 1); | ||
| for (size_t i = 1; i < reader_second_buffer.length; i++) { | ||
| le_invoke_command(le_current_mode->default_command, | ||
| reader_second_buffer.contents[i]); | ||
| } | ||
| wb_clear(&reader_second_buffer); | ||
| } else { | ||
| le_invoke_command(tg.value.cmdfunc, c); | ||
| wb_remove(&reader_second_buffer, 0, tg.matchlength); | ||
| } |
There was a problem hiding this comment.
Not sure if this is the best way to do it, also not sure if it's a good idea to start loop iteration at index 1 but this ensures that, when pasting a sequence like a\nb to the terminal, it is echo'ed as a^Jb by yash.
There was a problem hiding this comment.
In this implementation, a newline is inserted as two characters (^ and J). Given what bracketed paste is meant to do, I think it's better to insert newlines literally. A good way to do this would be to convert the input directly to wide characters and feed them into le_main_buffer before key codes are handled in this function's first loop. If this works, even though it'll still show up as ^J on screen, the cursor should treat each newline as a single character, and multi-line commands should parse as intended.
|
Thank you for the pull request. I think this feature is useful, but there is some room for discussion regarding the implementation. Please give me a little time to organize my thoughts. |
magicant
left a comment
There was a problem hiding this comment.
Thanks again for the pull request. After taking a closer look, I realized we need to make more design changes than I first thought to make this feature production-ready. If you're willing to keep working on it, please feel free to do so. If not, I can take this over when I have time.
| void update_le_bracketed_option(void) | ||
| { | ||
| const char *mode = (shopt_le_bracketed) ? | ||
| BRACKETED_PASTE_INIT : BRACKETED_PASTE_DENIT; | ||
| xprintf("%s", mode); | ||
| fflush(stdout); | ||
| } | ||
|
|
There was a problem hiding this comment.
This function is called only when the le-bracket-paste option is toggled. If you enable the option during shell startup and leave it enabled for the lifetime of the shell session, the terminal will assume that any utilities invoked from the shell are capable of handling the paste escape sequence. This will break utilities that lack this capability when you paste something into them.
My recommendation is to print BRACKETED_PASTE_INIT each time the line editor starts reading input, and BRACKETED_PASTE_DENIT each time it finishes. This way, the terminal will enable bracketed paste only while the shell is reading commands. The le_set_terminal and le_restore_terminal functions would be the best places to do this.
|
|
||
| /* See https://www.xfree86.org/4.7.0/ctlseqs.html#Bracketed%20Paste%20Mode */ | ||
| #define Key_br_start L"\\^[[200~" | ||
| #define Key_br_stop L"\\^[[201~" |
There was a problem hiding this comment.
Does the terminfo library provide definitions for these sequences? If so, it would be preferable to obtain them from the library rather than hard-code them here. Other sequences are handled in lineedit/terminfo.c.
| wb_remove(&reader_second_buffer, 0, tg.matchlength); | ||
| /* In bracketed paste mode only allow cmd_bracketed_paste_end | ||
| * to quit this mode, ignore all other commands/keybindings. */ | ||
| if (le_bracketed_paste && tg.value.cmdfunc != cmd_bracketed_paste_end) { |
There was a problem hiding this comment.
Since the paste escape sequences are handled as key-mapped commands, they make the existing mapping for Key_escape ambiguous in vi-insert mode. When hitting the escape key, the cursor is no longer repositioned until another key is pressed to disambiguate the mapping.
I think these sequences would be better handled as key codes in the first loop of this function, where an ambiguous Key_escape is disambiguated automatically by a timeout.
| #if YASH_ENABLE_LINEEDIT | ||
| { 0, 0, L"lealwaysrp", &shopt_le_alwaysrp, true, }, | ||
| { 0, 0, L"lecompdebug", &shopt_le_compdebug, true, }, | ||
| { 0, 0, L"lebracketpaste", &shopt_le_bracketed, true, }, |
There was a problem hiding this comment.
Sorry for nitpicking, but lebracketpaste should come before lecompdebug.
| if (le_bracketed_paste && tg.value.cmdfunc != cmd_bracketed_paste_end) { | ||
| assert(reader_second_buffer.length > 1); | ||
| for (size_t i = 1; i < reader_second_buffer.length; i++) { | ||
| le_invoke_command(le_current_mode->default_command, | ||
| reader_second_buffer.contents[i]); | ||
| } | ||
| wb_clear(&reader_second_buffer); | ||
| } else { | ||
| le_invoke_command(tg.value.cmdfunc, c); | ||
| wb_remove(&reader_second_buffer, 0, tg.matchlength); | ||
| } |
There was a problem hiding this comment.
In this implementation, a newline is inserted as two characters (^ and J). Given what bracketed paste is meant to do, I think it's better to insert newlines literally. A good way to do this would be to convert the input directly to wide characters and feed them into le_main_buffer before key codes are handled in this function's first loop. If this works, even though it'll still show up as ^J on screen, the cursor should treat each newline as a single character, and multi-line commands should parse as intended.
This patch adds support for bracketed paste mode to yash. Bracketed paste mode is a set of special escape sequences, which are employed by many terminal emulators to allow programs run inside of them to distinguish pasted text from typed-in text [0]. This is useful for preventing pasted text from accidentally executing commands in the application it was pasted to. Commonly this problem arises when copying text from a web browser to a shell since the user may have copied hidden text from the web page which may contain control characters [1].
Bracketed paste mode is supported by all mainstream terminal emulators. However, as implemented here, the feature must be explicitly enabled through a newly added option (
le-bracket-paste). By setting this option, the terminal emulator is instructed to emit the start/stop sequence once the user starts pasting text. Disabling the option again configures the terminal emulator to no longer emit these start/stop sequences.Currently, bracketed paste mode is only enabled in Vi insert mode and Emacs mode, we could consider enabling it in an additional modes in the future (e.g., the search mode).
For more information, refer to https://invisible-island.net/xterm/xterm-paste64.html
I just discovered yash a few days ago, so I haven't tested this extensively yet, but this was the first feature that I found to be lacking in yash. Hence, I have written this patch. Let me know if you are generally interested in this feature. Otherwise I will just apply it locally. Also looking for feedback on the implementation, ofc :)