fux 0.12.0

A minimal trusted Bevy terminal multiplexer
//! A bounded bracketed-paste envelope around Termina's native input decoder.
//! The start event lets the server retain ownership even if a modal is cancelled
//! by another control request before the terminal sends the end marker.
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>,
}

/// Every newly inserted overlay gets a fresh serial, whichever code path opened
/// it, so a paste captured for an earlier overlay can never land in this one.
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;
/// Bytes fux adds around an accepted paste when the application requested
/// bracketed-paste mode; the PTY transport budget covers `LIMIT + ENVELOPE`.
pub const ENVELOPE: usize = START.len() + END.len();

/// Frames an accepted paste for an application that requested bracketed paste.
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(())
    }
}