Add bracketed paste mode support for handling pasted text as verbatim input

This commit is contained in:
2026-03-05 20:04:20 -05:00
parent cac7140c8b
commit e31e27f935
7 changed files with 95 additions and 24 deletions

View File

@@ -297,7 +297,7 @@ pub fn read_key(node: Node) -> ShResult<()> {
} }
Ok(n) => { Ok(n) => {
let mut reader = PollReader::new(); let mut reader = PollReader::new();
reader.feed_bytes(&buf[..n], false); reader.feed_bytes(&buf[..n]);
let Some(key) = reader.read_key()? else { let Some(key) = reader.read_key()? else {
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());

View File

@@ -32,6 +32,7 @@ use crate::libsh::sys::TTY_FILENO;
use crate::libsh::utils::AutoCmdVecUtils; use crate::libsh::utils::AutoCmdVecUtils;
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::procio::borrow_fd;
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; 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 { } else if let Some(cmd) = args.command {
exec_input(cmd, None, false, None) exec_input(cmd, None, false, None)
} else { } 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(); 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 // Main poll loop
loop { loop {
write_meta(|m| { write_meta(|m| {

View File

@@ -2184,7 +2184,7 @@ impl LineBuf {
let Some(pos) = self.find_next_matching_delim() else { let Some(pos) = self.find_next_matching_delim() else {
return MotionKind::Null; return MotionKind::Null;
}; };
MotionKind::On(pos) MotionKind::Onto(pos)
} }
MotionCmd(_, Motion::ToBrace(direction)) MotionCmd(_, Motion::ToBrace(direction))
| MotionCmd(_, Motion::ToBracket(direction)) | MotionCmd(_, Motion::ToBracket(direction))

View File

@@ -300,8 +300,7 @@ impl ShedVi {
/// Feed raw bytes from stdin into the reader's buffer /// Feed raw bytes from stdin into the reader's buffer
pub fn feed_bytes(&mut self, bytes: &[u8]) { pub fn feed_bytes(&mut self, bytes: &[u8]) {
let verbatim = self.mode.report_mode() == ModeReport::Verbatim; self.reader.feed_bytes(bytes);
self.reader.feed_bytes(bytes, verbatim);
} }
/// Mark that the display needs to be redrawn (e.g., after SIGWINCH) /// Mark that the display needs to be redrawn (e.g., after SIGWINCH)
@@ -405,6 +404,7 @@ impl ShedVi {
// Process all available keys // Process all available keys
while let Some(key) = self.reader.read_key()? { 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 completer or history search are active, delegate input to it
if self.history.fuzzy_finder.is_active() { if self.history.fuzzy_finder.is_active() {
self.print_line(false)?; self.print_line(false)?;
@@ -1048,7 +1048,7 @@ impl ShedVi {
Verb::ExMode => Box::new(ViEx::new()), 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()), Verb::NormalMode => Box::new(ViNormal::new()),

View File

@@ -444,6 +444,8 @@ impl Perform for KeyCollector {
21 => KeyCode::F(10), 21 => KeyCode::F(10),
23 => KeyCode::F(11), 23 => KeyCode::F(11),
24 => KeyCode::F(12), 24 => KeyCode::F(12),
200 => KeyCode::BracketedPasteStart,
201 => KeyCode::BracketedPasteEnd,
_ => return, _ => return,
}; };
KeyEvent(key, mods) KeyEvent(key, mods)
@@ -496,6 +498,8 @@ impl Perform for KeyCollector {
pub struct PollReader { pub struct PollReader {
parser: Parser, parser: Parser,
collector: KeyCollector, collector: KeyCollector,
byte_buf: VecDeque<u8>,
pub verbatim: bool,
} }
impl PollReader { impl PollReader {
@@ -503,25 +507,32 @@ impl PollReader {
Self { Self {
parser: Parser::new(), parser: Parser::new(),
collector: KeyCollector::new(), collector: KeyCollector::new(),
byte_buf: VecDeque::new(),
verbatim: false,
} }
} }
pub fn feed_bytes(&mut self, bytes: &[u8], verbatim: bool) { pub fn handle_bracket_paste(&mut self) -> Option<KeyEvent> {
if verbatim { let end_marker = b"\x1b[201~";
let seq = String::from_utf8_lossy(bytes).to_string(); let mut raw = vec![];
self.collector.push(KeyEvent( while let Some(byte) = self.byte_buf.pop_front() {
KeyCode::Verbatim(Arc::from(seq.as_str())), raw.push(byte);
ModKeys::empty(), if raw.ends_with(end_marker) {
)); // Strip the end marker from the raw sequence
} else if bytes == [b'\x1b'] { raw.truncate(raw.len() - end_marker.len());
// Single escape byte - user pressed ESC key let paste = String::from_utf8_lossy(&raw).to_string();
self self.verbatim = false;
.collector return Some(KeyEvent(KeyCode::Verbatim(paste.into()), ModKeys::empty()));
.push(KeyEvent(KeyCode::Esc, ModKeys::empty())); }
} else { }
// Feed all bytes through vte parser
self.parser.advance(&mut self.collector, bytes); 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 { impl KeyReader for PollReader {
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> { fn read_key(&mut self) -> Result<Option<KeyEvent>, 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)
} }
} }

View File

@@ -61,6 +61,10 @@ impl ViMode for ViInsert {
raw_seq: String::new(), raw_seq: String::new(),
flags: Default::default(), 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) => { E(K::Char('W'), M::CTRL) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete)); self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_motion(MotionCmd( self.pending_cmd.set_motion(MotionCmd(

View File

@@ -4,11 +4,19 @@ use crate::readline::vicmd::{CmdFlags, RegisterName, To, Verb, VerbCmd, ViCmd};
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct ViVerbatim { pub struct ViVerbatim {
pending_seq: String,
sent_cmd: Vec<ViCmd>, sent_cmd: Vec<ViCmd>,
repeat_count: u16, repeat_count: u16,
read_one: bool
} }
impl ViVerbatim { impl ViVerbatim {
pub fn read_one() -> Self {
Self {
read_one: true,
..Self::default()
}
}
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@@ -23,7 +31,7 @@ impl ViVerbatim {
impl ViMode for ViVerbatim { impl ViMode for ViVerbatim {
fn handle_key(&mut self, key: E) -> Option<ViCmd> { fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key { match key {
E(K::Verbatim(seq), _mods) => { E(K::Verbatim(seq), _mods) if self.read_one => {
log::debug!("Received verbatim key sequence: {:?}", seq); log::debug!("Received verbatim key sequence: {:?}", seq);
let cmd = ViCmd { let cmd = ViCmd {
register: RegisterName::default(), register: RegisterName::default(),
@@ -35,6 +43,22 @@ impl ViMode for ViVerbatim {
self.sent_cmd.push(cmd.clone()); self.sent_cmd.push(cmd.clone());
Some(cmd) 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), _ => common_cmds(key),
} }
} }