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    /// Opaque tmux DCS passthrough payload, already framed for the outer terminal.
19    Raw,
20    /// OSC 52 clipboard payload emitted by a pane program.
21    Clipboard,
22    /// Kitty terminal graphics protocol, encoded as an APC payload.
23    KittyGraphics,
24    /// SIXEL graphics protocol, encoded as a DCS payload.
25    Sixel,
26}
27
28impl TerminalPassthrough {
29    /// Creates an opaque passthrough event at a pane-local cursor position.
30    #[must_use]
31    pub fn raw(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
32        Self {
33            kind: TerminalPassthroughKind::Raw,
34            cursor_x,
35            cursor_y,
36            payload: Arc::from(payload.into()),
37        }
38    }
39
40    /// Creates an OSC 52 clipboard passthrough event.
41    #[must_use]
42    pub fn clipboard(payload: impl Into<Vec<u8>>) -> Self {
43        Self {
44            kind: TerminalPassthroughKind::Clipboard,
45            cursor_x: 0,
46            cursor_y: 0,
47            payload: Arc::from(payload.into()),
48        }
49    }
50
51    /// Creates a Kitty graphics passthrough event at a pane-local cursor position.
52    #[must_use]
53    pub fn kitty_graphics(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
54        Self {
55            kind: TerminalPassthroughKind::KittyGraphics,
56            cursor_x,
57            cursor_y,
58            payload: Arc::from(payload.into()),
59        }
60    }
61
62    /// Creates a SIXEL passthrough event at a pane-local cursor position.
63    #[must_use]
64    pub fn sixel(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
65        Self {
66            kind: TerminalPassthroughKind::Sixel,
67            cursor_x,
68            cursor_y,
69            payload: Arc::from(payload.into()),
70        }
71    }
72
73    /// Returns the passthrough protocol family.
74    #[must_use]
75    pub const fn kind(&self) -> TerminalPassthroughKind {
76        self.kind
77    }
78
79    /// Returns the pane-local cursor column captured when the sequence arrived.
80    #[must_use]
81    pub const fn cursor_x(&self) -> u32 {
82        self.cursor_x
83    }
84
85    /// Returns the pane-local cursor row captured when the sequence arrived.
86    #[must_use]
87    pub const fn cursor_y(&self) -> u32 {
88        self.cursor_y
89    }
90
91    /// Returns the opaque protocol payload without escape framing.
92    #[must_use]
93    pub fn payload(&self) -> &[u8] {
94        &self.payload
95    }
96
97    /// Renders the passthrough as an outer-terminal escape sequence.
98    #[must_use]
99    pub fn render_sequence(&self) -> Vec<u8> {
100        match self.kind {
101            TerminalPassthroughKind::Raw => self.payload.to_vec(),
102            TerminalPassthroughKind::Clipboard => self.payload.to_vec(),
103            TerminalPassthroughKind::KittyGraphics => {
104                let mut sequence = Vec::with_capacity(self.payload.len() + 4);
105                sequence.extend_from_slice(b"\x1b_");
106                sequence.extend_from_slice(&self.payload);
107                sequence.extend_from_slice(b"\x1b\\");
108                sequence
109            }
110            TerminalPassthroughKind::Sixel => {
111                let mut sequence = Vec::with_capacity(self.payload.len() + 4);
112                sequence.extend_from_slice(b"\x1bP");
113                sequence.extend_from_slice(&self.payload);
114                sequence.extend_from_slice(b"\x1b\\");
115                sequence
116            }
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::TerminalPassthrough;
124
125    #[test]
126    fn renders_kitty_apc_sequence() {
127        let passthrough = TerminalPassthrough::kitty_graphics(0, 0, b"Gf=100;AAAA".to_vec());
128
129        assert_eq!(passthrough.render_sequence(), b"\x1b_Gf=100;AAAA\x1b\\");
130    }
131
132    #[test]
133    fn renders_raw_sequence_verbatim() {
134        let passthrough = TerminalPassthrough::raw(0, 0, b"\x1b]52;c;QQ==\x1b\\".to_vec());
135
136        assert_eq!(passthrough.render_sequence(), b"\x1b]52;c;QQ==\x1b\\");
137    }
138
139    #[test]
140    fn renders_clipboard_sequence_verbatim() {
141        let passthrough = TerminalPassthrough::clipboard(b"\x1b]52;c;QQ==\x07".to_vec());
142
143        assert_eq!(passthrough.render_sequence(), b"\x1b]52;c;QQ==\x07");
144    }
145
146    #[test]
147    fn renders_sixel_dcs_sequence() {
148        let passthrough = TerminalPassthrough::sixel(0, 0, b"q#0!10~".to_vec());
149
150        assert_eq!(passthrough.render_sequence(), b"\x1bPq#0!10~\x1b\\");
151    }
152}