diff options
| author | akbash <akbash@herdr.dev> | 2026-08-21 15:16:43 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-21 15:16:43 +0300 |
| commit | 8fe09d5ddf77da5c377760682b33b386ebc37640 (patch) | |
| tree | c63a23bb630d17e8347c93dc1b01e3ff95bfc531 /src/client | |
| parent | 624dfd4796559042ec13ccf4d4b54374902ab81d (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.rs | 49 |
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!( |
