Skip to main content

rmux_core/
terminal_passthrough.rs

1use std::sync::Arc;
2
3/// Maximum payload size retained for one terminal graphics passthrough event.
4pub(crate) const MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES: usize = 8 * 1024 * 1024;
5
6/// Opaque terminal command that must be forwarded to a capable outer terminal.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct TerminalPassthrough {
9    kind: TerminalPassthroughKind,
10    cursor_x: u32,
11    cursor_y: u32,
12    payload: Arc<[u8]>,
13}
14
15/// Supported terminal passthrough protocol families.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum TerminalPassthroughKind {
18    /// Kitty terminal graphics protocol, encoded as an APC payload.
19    KittyGraphics,
20    /// SIXEL graphics protocol, encoded as a DCS payload.
21    Sixel,
22}
23
24impl TerminalPassthrough {
25    /// Creates a Kitty graphics passthrough event at a pane-local cursor position.
26    #[must_use]
27    pub fn kitty_graphics(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
28        Self {
29            kind: TerminalPassthroughKind::KittyGraphics,
30            cursor_x,
31            cursor_y,
32            payload: Arc::from(payload.into()),
33        }
34    }
35
36    /// Creates a SIXEL passthrough event at a pane-local cursor position.
37    #[must_use]
38    pub fn sixel(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
39        Self {
40            kind: TerminalPassthroughKind::Sixel,
41            cursor_x,
42            cursor_y,
43            payload: Arc::from(payload.into()),
44        }
45    }
46
47    /// Returns the passthrough protocol family.
48    #[must_use]
49    pub const fn kind(&self) -> TerminalPassthroughKind {
50        self.kind
51    }
52
53    /// Returns the pane-local cursor column captured when the sequence arrived.
54    #[must_use]
55    pub const fn cursor_x(&self) -> u32 {
56        self.cursor_x
57    }
58
59    /// Returns the pane-local cursor row captured when the sequence arrived.
60    #[must_use]
61    pub const fn cursor_y(&self) -> u32 {
62        self.cursor_y
63    }
64
65    /// Returns the opaque protocol payload without escape framing.
66    #[must_use]
67    pub fn payload(&self) -> &[u8] {
68        &self.payload
69    }
70
71    /// Renders the passthrough as an outer-terminal escape sequence.
72    #[must_use]
73    pub fn render_sequence(&self) -> Vec<u8> {
74        match self.kind {
75            TerminalPassthroughKind::KittyGraphics => {
76                let mut sequence = Vec::with_capacity(self.payload.len() + 4);
77                sequence.extend_from_slice(b"\x1b_");
78                sequence.extend_from_slice(&self.payload);
79                sequence.extend_from_slice(b"\x1b\\");
80                sequence
81            }
82            TerminalPassthroughKind::Sixel => {
83                let mut sequence = Vec::with_capacity(self.payload.len() + 4);
84                sequence.extend_from_slice(b"\x1bP");
85                sequence.extend_from_slice(&self.payload);
86                sequence.extend_from_slice(b"\x1b\\");
87                sequence
88            }
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::TerminalPassthrough;
96
97    #[test]
98    fn renders_kitty_apc_sequence() {
99        let passthrough = TerminalPassthrough::kitty_graphics(0, 0, b"Gf=100;AAAA".to_vec());
100
101        assert_eq!(passthrough.render_sequence(), b"\x1b_Gf=100;AAAA\x1b\\");
102    }
103
104    #[test]
105    fn renders_sixel_dcs_sequence() {
106        let passthrough = TerminalPassthrough::sixel(0, 0, b"q#0!10~".to_vec());
107
108        assert_eq!(passthrough.render_sequence(), b"\x1bPq#0!10~\x1b\\");
109    }
110}