aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-08-21 15:16:43 +0300
committerGitHub <noreply@github.com>2026-08-21 15:16:43 +0300
commit8fe09d5ddf77da5c377760682b33b386ebc37640 (patch)
treec63a23bb630d17e8347c93dc1b01e3ff95bfc531 /src/client
parent624dfd4796559042ec13ccf4d4b54374902ab81d (diff)
fix: preserve multiline paste in terminal attach (#3056)
refs #3054 Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Diffstat (limited to 'src/client')
-rw-r--r--src/client/mod.rs49
1 files changed, 49 insertions, 0 deletions
diff --git a/src/client/mod.rs b/src/client/mod.rs
index 27a2e9c4..a5493ca2 100644
--- a/src/client/mod.rs
+++ b/src/client/mod.rs
@@ -114,6 +114,7 @@ struct AttachEscapeState {
#[cfg(unix)]
enum AttachInputAction {
Forward(Vec<u8>),
+ ForwardAfterPendingPrefix(Vec<u8>),
Scroll {
source: AttachScrollSource,
direction: AttachScrollDirection,
@@ -136,6 +137,14 @@ impl AttachEscapeState {
) -> AttachInputAction {
const PREFIX: u8 = 0x02; // Ctrl+B
+ if crate::raw_input::is_complete_text_bracketed_paste(&data) {
+ return if std::mem::take(&mut self.pending_prefix) {
+ AttachInputAction::ForwardAfterPendingPrefix(data)
+ } else {
+ AttachInputAction::Forward(data)
+ };
+ }
+
let mut output = Vec::with_capacity(data.len());
for byte in data {
if self.pending_prefix {
@@ -384,6 +393,7 @@ fn setup_terminal_with_capabilities(
} else {
set_mouse_capture(false, false)?;
}
+ execute!(io::stdout(), EnableBracketedPaste)?;
}
#[cfg(windows)]
@@ -1528,6 +1538,13 @@ async fn run_client_loop(
state.mouse_scroll_lines,
) {
AttachInputAction::Forward(data) => data,
+ AttachInputAction::ForwardAfterPendingPrefix(data) => {
+ let prefix = ClientMessage::Input { data: vec![0x02] };
+ if let Err(e) = write_to_server(&mut write_stream, &prefix) {
+ return Err(ClientError::ConnectionLost(e));
+ }
+ data
+ }
AttachInputAction::Scroll {
source,
direction,
@@ -3135,6 +3152,38 @@ mod tests {
#[cfg(unix)]
#[test]
+ fn attach_escape_does_not_interpret_bracketed_paste_contents() {
+ let mut escape = AttachEscapeState::default();
+ let paste = b"\x1b[200~one\x02q\ntwo\x1b[201~".to_vec();
+
+ match escape.filter_input(paste.clone(), 24, 3) {
+ AttachInputAction::Forward(bytes) => assert_eq!(bytes, paste),
+ other => panic!("expected opaque paste, got {other:?}"),
+ }
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn attach_escape_flushes_pending_prefix_before_bracketed_paste() {
+ let mut escape = AttachEscapeState::default();
+ let paste = b"\x1b[200~one\ntwo\x1b[201~".to_vec();
+ assert!(matches!(
+ escape.filter_input(vec![0x02], 24, 3),
+ AttachInputAction::None
+ ));
+
+ assert!(matches!(
+ escape.filter_input(paste.clone(), 24, 3),
+ AttachInputAction::ForwardAfterPendingPrefix(bytes) if bytes == paste
+ ));
+ assert!(matches!(
+ escape.filter_input(vec![b'q'], 24, 3),
+ AttachInputAction::Forward(bytes) if bytes == b"q"
+ ));
+ }
+
+ #[cfg(unix)]
+ #[test]
fn attach_escape_forwards_prefix_before_non_escape_key() {
let mut escape = AttachEscapeState::default();
assert!(matches!(