From e31e27f935631c10430afd6d6527304028be4395 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Thu, 5 Mar 2026 20:04:20 -0500 Subject: [PATCH] Add bracketed paste mode support for handling pasted text as verbatim input --- src/builtin/read.rs | 2 +- src/main.rs | 7 +++- src/readline/linebuf.rs | 2 +- src/readline/mod.rs | 6 +-- src/readline/term.rs | 72 +++++++++++++++++++++++++-------- src/readline/vimode/insert.rs | 4 ++ src/readline/vimode/verbatim.rs | 26 +++++++++++- 7 files changed, 95 insertions(+), 24 deletions(-) diff --git a/src/builtin/read.rs b/src/builtin/read.rs index e866150..935e93f 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -297,7 +297,7 @@ pub fn read_key(node: Node) -> ShResult<()> { } Ok(n) => { let mut reader = PollReader::new(); - reader.feed_bytes(&buf[..n], false); + reader.feed_bytes(&buf[..n]); let Some(key) = reader.read_key()? else { state::set_status(1); return Ok(()); diff --git a/src/main.rs b/src/main.rs index 24b0e74..bee2e16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ use crate::libsh::sys::TTY_FILENO; use crate::libsh::utils::AutoCmdVecUtils; use crate::parse::execute::exec_input; use crate::prelude::*; +use crate::procio::borrow_fd; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; @@ -131,7 +132,9 @@ fn main() -> ExitCode { } else if let Some(cmd) = args.command { exec_input(cmd, None, false, None) } else { - shed_interactive(args) + let res = shed_interactive(args); + write(borrow_fd(*TTY_FILENO), b"\x1b[?2004l").ok(); // disable bracketed paste mode on exit + res } { e.print_error(); }; @@ -201,6 +204,8 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> { } }; + readline.writer.flush_write("\x1b[?2004h")?; // enable bracketed paste mode + // Main poll loop loop { write_meta(|m| { diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index bc2ba90..30312d4 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -2184,7 +2184,7 @@ impl LineBuf { let Some(pos) = self.find_next_matching_delim() else { return MotionKind::Null; }; - MotionKind::On(pos) + MotionKind::Onto(pos) } MotionCmd(_, Motion::ToBrace(direction)) | MotionCmd(_, Motion::ToBracket(direction)) diff --git a/src/readline/mod.rs b/src/readline/mod.rs index ddfe350..0fb15a1 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -300,8 +300,7 @@ impl ShedVi { /// Feed raw bytes from stdin into the reader's buffer pub fn feed_bytes(&mut self, bytes: &[u8]) { - let verbatim = self.mode.report_mode() == ModeReport::Verbatim; - self.reader.feed_bytes(bytes, verbatim); + self.reader.feed_bytes(bytes); } /// Mark that the display needs to be redrawn (e.g., after SIGWINCH) @@ -405,6 +404,7 @@ impl ShedVi { // Process all available keys while let Some(key) = self.reader.read_key()? { + log::debug!("Read key: {key:?} in mode {:?}, self.reader.verbatim = {}", self.mode.report_mode(), self.reader.verbatim); // If completer or history search are active, delegate input to it if self.history.fuzzy_finder.is_active() { self.print_line(false)?; @@ -1048,7 +1048,7 @@ impl ShedVi { Verb::ExMode => Box::new(ViEx::new()), - Verb::VerbatimMode => Box::new(ViVerbatim::new().with_count(count as u16)), + Verb::VerbatimMode => Box::new(ViVerbatim::read_one().with_count(count as u16)), Verb::NormalMode => Box::new(ViNormal::new()), diff --git a/src/readline/term.rs b/src/readline/term.rs index 1142ba8..37dc9dc 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -444,6 +444,8 @@ impl Perform for KeyCollector { 21 => KeyCode::F(10), 23 => KeyCode::F(11), 24 => KeyCode::F(12), + 200 => KeyCode::BracketedPasteStart, + 201 => KeyCode::BracketedPasteEnd, _ => return, }; KeyEvent(key, mods) @@ -496,6 +498,8 @@ impl Perform for KeyCollector { pub struct PollReader { parser: Parser, collector: KeyCollector, + byte_buf: VecDeque, + pub verbatim: bool, } impl PollReader { @@ -503,25 +507,32 @@ impl PollReader { Self { parser: Parser::new(), collector: KeyCollector::new(), + byte_buf: VecDeque::new(), + verbatim: false, } } - pub fn feed_bytes(&mut self, bytes: &[u8], verbatim: bool) { - if verbatim { - let seq = String::from_utf8_lossy(bytes).to_string(); - self.collector.push(KeyEvent( - KeyCode::Verbatim(Arc::from(seq.as_str())), - ModKeys::empty(), - )); - } else if bytes == [b'\x1b'] { - // Single escape byte - user pressed ESC key - self - .collector - .push(KeyEvent(KeyCode::Esc, ModKeys::empty())); - } else { - // Feed all bytes through vte parser - self.parser.advance(&mut self.collector, bytes); - } + pub fn handle_bracket_paste(&mut self) -> Option { + let end_marker = b"\x1b[201~"; + let mut raw = vec![]; + while let Some(byte) = self.byte_buf.pop_front() { + raw.push(byte); + if raw.ends_with(end_marker) { + // Strip the end marker from the raw sequence + raw.truncate(raw.len() - end_marker.len()); + let paste = String::from_utf8_lossy(&raw).to_string(); + self.verbatim = false; + return Some(KeyEvent(KeyCode::Verbatim(paste.into()), ModKeys::empty())); + } + } + + self.verbatim = true; + self.byte_buf.extend(raw); + None + } + + pub fn feed_bytes(&mut self, bytes: &[u8]) { + self.byte_buf.extend(bytes); } } @@ -533,7 +544,34 @@ impl Default for PollReader { impl KeyReader for PollReader { fn read_key(&mut self) -> Result, ShErr> { - Ok(self.collector.pop()) + if self.verbatim { + if let Some(paste) = self.handle_bracket_paste() { + return Ok(Some(paste)); + } + // If we're in verbatim mode but haven't seen the end marker yet, don't attempt to parse keys + return Ok(None); + } else if self.byte_buf.len() == 1 + && self.byte_buf.front() == Some(&b'\x1b') { + // User pressed escape + self.byte_buf.pop_front(); // Consume the escape byte + return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty()))); + } + while let Some(byte) = self.byte_buf.pop_front() { + self.parser.advance(&mut self.collector, &[byte]); + if let Some(key) = self.collector.pop() { + match key { + KeyEvent(KeyCode::BracketedPasteStart, _) => { + if let Some(paste) = self.handle_bracket_paste() { + return Ok(Some(paste)); + } else { + continue; + } + } + _ => return Ok(Some(key)) + } + } + } + Ok(None) } } diff --git a/src/readline/vimode/insert.rs b/src/readline/vimode/insert.rs index c7c74ca..e314355 100644 --- a/src/readline/vimode/insert.rs +++ b/src/readline/vimode/insert.rs @@ -61,6 +61,10 @@ impl ViMode for ViInsert { raw_seq: String::new(), flags: Default::default(), }), + E(K::Verbatim(seq), _) => { + self.pending_cmd.set_verb(VerbCmd(1, Verb::Insert(seq.to_string()))); + self.register_and_return() + } E(K::Char('W'), M::CTRL) => { self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); self.pending_cmd.set_motion(MotionCmd( diff --git a/src/readline/vimode/verbatim.rs b/src/readline/vimode/verbatim.rs index 0673133..7dad52f 100644 --- a/src/readline/vimode/verbatim.rs +++ b/src/readline/vimode/verbatim.rs @@ -4,11 +4,19 @@ use crate::readline::vicmd::{CmdFlags, RegisterName, To, Verb, VerbCmd, ViCmd}; #[derive(Default, Clone, Debug)] pub struct ViVerbatim { + pending_seq: String, sent_cmd: Vec, repeat_count: u16, + read_one: bool } impl ViVerbatim { + pub fn read_one() -> Self { + Self { + read_one: true, + ..Self::default() + } + } pub fn new() -> Self { Self::default() } @@ -23,7 +31,7 @@ impl ViVerbatim { impl ViMode for ViVerbatim { fn handle_key(&mut self, key: E) -> Option { match key { - E(K::Verbatim(seq), _mods) => { + E(K::Verbatim(seq), _mods) if self.read_one => { log::debug!("Received verbatim key sequence: {:?}", seq); let cmd = ViCmd { register: RegisterName::default(), @@ -35,6 +43,22 @@ impl ViMode for ViVerbatim { self.sent_cmd.push(cmd.clone()); Some(cmd) } + E(K::Verbatim(seq), _mods) => { + self.pending_seq.push_str(&seq); + None + } + E(K::BracketedPasteEnd, _mods) => { + log::debug!("Received verbatim paste: {:?}", self.pending_seq); + let cmd = ViCmd { + register: RegisterName::default(), + verb: Some(VerbCmd(1, Verb::Insert(self.pending_seq.clone()))), + motion: None, + raw_seq: std::mem::take(&mut self.pending_seq), + flags: CmdFlags::EXIT_CUR_MODE, + }; + self.sent_cmd.push(cmd.clone()); + Some(cmd) + } _ => common_cmds(key), } }