Skip to content

Add support for bracketed paste mode - #254

Open
nmeum wants to merge 1 commit into
magicant:trunkfrom
nmeum:bracketed-paste
Open

Add support for bracketed paste mode#254
nmeum wants to merge 1 commit into
magicant:trunkfrom
nmeum:bracketed-paste

Conversation

@nmeum

@nmeum nmeum commented Aug 23, 2026

Copy link
Copy Markdown

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 :)

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
@nmeum
nmeum requested a review from magicant as a code owner August 23, 2026 13:58
Comment thread lineedit/lineedit.c
Comment on lines +339 to +349
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);
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@magicant

Copy link
Copy Markdown
Owner

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 magicant left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread option.c
Comment on lines +750 to +757
void update_le_bracketed_option(void)
{
const char *mode = (shopt_le_bracketed) ?
BRACKETED_PASTE_INIT : BRACKETED_PASTE_DENIT;
xprintf("%s", mode);
fflush(stdout);
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lineedit/key.h

/* 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~"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lineedit/lineedit.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) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread option.c
#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, },

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for nitpicking, but lebracketpaste should come before lecompdebug.

Comment thread lineedit/lineedit.c
Comment on lines +339 to +349
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);
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants