use crate::protocol::Input;
use crate::{
actions::Target,
interaction::{Mode, Overlay},
model::{Notice, Viewer},
};
use bevy_ecs::{lifecycle::HookContext, prelude::*, world::DeferredWorld};
#[derive(Component, Default)]
pub struct Ownership {
pub serial: u64,
pending: Option<Owner>,
}
pub(crate) fn overlay_opened(mut world: DeferredWorld, context: HookContext) {
if let Some(mut ownership) = world.get_mut::<Ownership>(context.entity) {
ownership.serial = ownership.serial.wrapping_add(1);
let serial = ownership.serial;
if let Some(mut overlay) = world.get_mut::<Overlay>(context.entity) {
overlay.serial = serial;
}
}
}
enum Owner {
Text(u64),
Pane(Target),
Discard,
}
pub fn input(world: &mut World, id: Entity, input: &Input) -> bool {
if matches!(input, Input::PasteBegin) {
if world.get::<Viewer>(id).is_none() {
return true;
}
let owner = if let Some(overlay) = world.get::<Overlay>(id) {
if matches!(overlay.mode, Mode::Text { .. }) {
Owner::Text(overlay.serial)
} else {
Owner::Discard
}
} else if crate::interaction::modal(world, id) {
Owner::Discard
} else {
match Target::of(world, id) {
Some(target) => Owner::Pane(target),
None => Owner::Discard,
}
};
if let Some(mut ownership) = world.get_mut::<Ownership>(id) {
ownership.pending = Some(owner);
}
crate::model::notify(world, id, Notice::info("pasting..."));
return true;
}
let Input::Paste { text } = input else {
return false;
};
let owner = world
.get_mut::<Ownership>(id)
.and_then(|mut state| state.pending.take());
if let Some(mut v) = world.get_mut::<Viewer>(id)
&& v.notice.as_ref().is_some_and(|n| n.text == "pasting...")
{
v.notice = None;
}
let valid = match owner {
None => true,
Some(Owner::Discard) => return true,
Some(Owner::Text(serial)) => world.get::<Overlay>(id).is_some_and(|o| o.serial == serial),
Some(Owner::Pane(target)) => {
Target::of(world, id) == Some(target) && !crate::interaction::modal(world, id)
}
};
if !valid || text.len() > LIMIT {
let reason = if valid {
"paste exceeds 64 KiB; discarded"
} else {
"paste owner changed; discarded"
};
crate::model::notify(world, id, Notice::error(reason));
return true;
}
false
}
const START: &[u8] = b"\x1b[200~";
const END: &[u8] = b"\x1b[201~";
pub const LIMIT: usize = 64 * 1024;
pub const ENVELOPE: usize = START.len() + END.len();
pub fn bracketed(text: &str) -> Vec<u8> {
let mut bytes = Vec::with_capacity(text.len() + ENVELOPE);
bytes.extend_from_slice(START);
bytes.extend_from_slice(text.as_bytes());
bytes.extend_from_slice(END);
bytes
}
#[derive(Default)]
pub struct Decoder {
parser: termina::Parser,
boundary: Vec<u8>,
paste: Option<Vec<u8>>,
incomplete: bool,
}
impl Decoder {
pub fn deadline_needed(&self) -> bool {
self.paste.is_none() && (self.incomplete || !self.boundary.is_empty())
}
fn ordinary(&mut self, bytes: &[u8], more: bool, emit: &mut impl FnMut(Input)) {
self.parser.parse(bytes, more);
self.incomplete = more;
while let Some(event) = self.parser.pop() {
self.incomplete = false;
if let Some(input) = crate::viewer::convert(event) {
emit(input);
}
}
}
pub fn bytes(&mut self, bytes: &[u8], mut emit: impl FnMut(Input)) {
for &byte in bytes {
self.boundary.push(byte);
let delimiter = if self.paste.is_some() { END } else { START };
while !delimiter.starts_with(&self.boundary) {
let byte = self.boundary.remove(0);
if let Some(paste) = &mut self.paste {
if paste.len() <= LIMIT {
paste.push(byte);
}
} else {
self.ordinary(&[byte], true, &mut emit);
}
}
if self.boundary == delimiter {
self.boundary.clear();
if let Some(paste) = self.paste.take() {
emit(Input::Paste {
text: String::from_utf8_lossy(&paste).into_owned(),
});
} else {
self.paste = Some(Vec::new());
self.incomplete = false;
emit(Input::PasteBegin);
}
}
}
}
pub fn timeout(&mut self, mut emit: impl FnMut(Input)) {
if self.paste.is_some() {
return;
}
let boundary = std::mem::take(&mut self.boundary);
self.ordinary(&boundary, false, &mut emit);
self.incomplete = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::Key;
use crate::testing::*;
#[test]
fn every_fragment_boundary_preserves_paste_ownership_and_embedded_escape()
-> crate::testing::Outcome {
let bytes = b"\x02r\x1b[200~one\x1btwo\x1b[201~\x1b";
for chunk in 1..=bytes.len() {
let mut decoder = Decoder::default();
let mut events = Vec::new();
for bytes in bytes.chunks(chunk) {
decoder.bytes(bytes, |event| events.push(event));
}
decoder.timeout(|event| events.push(event));
assert!(matches!(events.get(2), Some(Input::PasteBegin)));
assert!(matches!(events.get(3), Some(Input::Paste { text }) if text == "one\x1btwo"));
assert!(matches!(events.get(4), Some(Input::Key { key, .. }) if *key == Key::Escape));
assert_eq!(events.len(), 5);
}
Ok(())
}
#[test]
fn largest_accepted_paste_fits_the_transport_with_its_envelope() {
let text = "x".repeat(LIMIT);
let framed = bracketed(&text);
assert!(framed.starts_with(START) && framed.ends_with(END));
assert_eq!(framed.len(), LIMIT + ENVELOPE);
assert_eq!(
framed.get(START.len()..framed.len() - END.len()),
Some(text.as_bytes())
);
}
#[test]
fn oversized_paste_is_bounded_and_drains_before_following_keys() -> crate::testing::Outcome {
let mut decoder = Decoder::default();
let mut events = Vec::new();
decoder.bytes(START, |e| events.push(e));
decoder.bytes(&vec![b'a'; LIMIT * 4], |e| events.push(e));
assert!(decoder.paste.as_ref().need()?.len() <= LIMIT + 1);
assert!(!decoder.deadline_needed());
decoder.bytes(b"\x1b[201~z", |e| events.push(e));
assert!(matches!(events.get(1), Some(Input::Paste { text }) if text.len() == LIMIT + 1));
assert!(matches!(events.get(2), Some(Input::Key { key, .. }) if *key == Key::Char('z')));
Ok(())
}
}