rmux_core/
terminal_passthrough.rs1use std::sync::Arc;
2
3pub(crate) const MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES: usize = 8 * 1024 * 1024;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct TerminalPassthrough {
9 kind: TerminalPassthroughKind,
10 cursor_x: u32,
11 cursor_y: u32,
12 palette_index: Option<TerminalPaletteIndex>,
13 payload: Arc<[u8]>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TerminalPassthroughKind {
19 Raw,
21 Clipboard,
23 PaletteQuery,
25 KittyGraphics,
27 Sixel,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub struct TerminalPaletteIndex(u8);
38
39impl TerminalPaletteIndex {
40 #[must_use]
43 pub fn parse(value: &str) -> Option<Self> {
44 if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
45 return None;
46 }
47 value.parse::<u8>().ok().map(Self)
48 }
49
50 #[must_use]
52 pub const fn get(self) -> u8 {
53 self.0
54 }
55}
56
57impl From<u8> for TerminalPaletteIndex {
58 fn from(value: u8) -> Self {
59 Self(value)
60 }
61}
62
63impl TerminalPassthrough {
64 #[must_use]
66 pub fn raw(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
67 Self {
68 kind: TerminalPassthroughKind::Raw,
69 cursor_x,
70 cursor_y,
71 palette_index: None,
72 payload: Arc::from(payload.into()),
73 }
74 }
75
76 #[must_use]
78 pub fn clipboard(payload: impl Into<Vec<u8>>) -> Self {
79 Self {
80 kind: TerminalPassthroughKind::Clipboard,
81 cursor_x: 0,
82 cursor_y: 0,
83 palette_index: None,
84 payload: Arc::from(payload.into()),
85 }
86 }
87
88 #[must_use]
93 pub fn palette_query(index: TerminalPaletteIndex) -> Self {
94 let payload = format!("\x1b]4;{};?\x1b\\", index.get()).into_bytes();
95 Self {
96 kind: TerminalPassthroughKind::PaletteQuery,
97 cursor_x: 0,
98 cursor_y: 0,
99 palette_index: Some(index),
100 payload: Arc::from(payload),
101 }
102 }
103
104 #[must_use]
106 pub fn kitty_graphics(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
107 Self {
108 kind: TerminalPassthroughKind::KittyGraphics,
109 cursor_x,
110 cursor_y,
111 palette_index: None,
112 payload: Arc::from(payload.into()),
113 }
114 }
115
116 #[must_use]
118 pub fn sixel(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
119 Self {
120 kind: TerminalPassthroughKind::Sixel,
121 cursor_x,
122 cursor_y,
123 palette_index: None,
124 payload: Arc::from(payload.into()),
125 }
126 }
127
128 #[must_use]
130 pub const fn kind(&self) -> TerminalPassthroughKind {
131 self.kind
132 }
133
134 #[must_use]
136 pub const fn cursor_x(&self) -> u32 {
137 self.cursor_x
138 }
139
140 #[must_use]
142 pub const fn cursor_y(&self) -> u32 {
143 self.cursor_y
144 }
145
146 #[must_use]
148 pub fn payload(&self) -> &[u8] {
149 &self.payload
150 }
151
152 #[must_use]
154 pub const fn palette_query_index(&self) -> Option<TerminalPaletteIndex> {
155 self.palette_index
156 }
157
158 #[must_use]
160 pub fn render_sequence(&self) -> Vec<u8> {
161 match self.kind {
162 TerminalPassthroughKind::Raw => self.payload.to_vec(),
163 TerminalPassthroughKind::Clipboard => self.payload.to_vec(),
164 TerminalPassthroughKind::PaletteQuery => self.payload.to_vec(),
165 TerminalPassthroughKind::KittyGraphics => {
166 let mut sequence = Vec::with_capacity(self.payload.len() + 4);
167 sequence.extend_from_slice(b"\x1b_");
168 sequence.extend_from_slice(&self.payload);
169 sequence.extend_from_slice(b"\x1b\\");
170 sequence
171 }
172 TerminalPassthroughKind::Sixel => {
173 let mut sequence = Vec::with_capacity(self.payload.len() + 4);
174 sequence.extend_from_slice(b"\x1bP");
175 sequence.extend_from_slice(&self.payload);
176 sequence.extend_from_slice(b"\x1b\\");
177 sequence
178 }
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::{TerminalPaletteIndex, TerminalPassthrough};
186
187 #[test]
188 fn renders_kitty_apc_sequence() {
189 let passthrough = TerminalPassthrough::kitty_graphics(0, 0, b"Gf=100;AAAA".to_vec());
190
191 assert_eq!(passthrough.render_sequence(), b"\x1b_Gf=100;AAAA\x1b\\");
192 }
193
194 #[test]
195 fn renders_raw_sequence_verbatim() {
196 let passthrough = TerminalPassthrough::raw(0, 0, b"\x1b]52;c;QQ==\x1b\\".to_vec());
197
198 assert_eq!(passthrough.render_sequence(), b"\x1b]52;c;QQ==\x1b\\");
199 }
200
201 #[test]
202 fn renders_clipboard_sequence_verbatim() {
203 let passthrough = TerminalPassthrough::clipboard(b"\x1b]52;c;QQ==\x07".to_vec());
204
205 assert_eq!(passthrough.render_sequence(), b"\x1b]52;c;QQ==\x07");
206 }
207
208 #[test]
209 fn palette_query_is_bounded_typed_and_canonical() {
210 assert_eq!(
211 TerminalPaletteIndex::parse("0").map(TerminalPaletteIndex::get),
212 Some(0)
213 );
214 assert_eq!(
215 TerminalPaletteIndex::parse("255").map(TerminalPaletteIndex::get),
216 Some(255)
217 );
218 assert_eq!(TerminalPaletteIndex::parse("256"), None);
219 assert_eq!(TerminalPaletteIndex::parse("-1"), None);
220
221 let query = TerminalPassthrough::palette_query(TerminalPaletteIndex::from(255));
222 assert_eq!(query.render_sequence(), b"\x1b]4;255;?\x1b\\");
223 assert_eq!(
224 query.palette_query_index(),
225 Some(TerminalPaletteIndex::from(255))
226 );
227 }
228
229 #[test]
230 fn renders_sixel_dcs_sequence() {
231 let passthrough = TerminalPassthrough::sixel(0, 0, b"q#0!10~".to_vec());
232
233 assert_eq!(passthrough.render_sequence(), b"\x1bPq#0!10~\x1b\\");
234 }
235}