1use std::sync::Arc;
2
3use crate::input::InputEndType;
4
5pub(crate) const MAX_TERMINAL_PASSTHROUGH_PAYLOAD_BYTES: usize = 8 * 1024 * 1024;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct TerminalPassthrough {
11 kind: TerminalPassthroughKind,
12 cursor_x: u32,
13 cursor_y: u32,
14 palette_index: Option<TerminalPaletteIndex>,
15 clipboard_query: Option<TerminalClipboardQuery>,
16 payload: Arc<[u8]>,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum TerminalPassthroughKind {
22 Raw,
24 Clipboard,
26 PaletteQuery,
28 KittyGraphics,
30 Sixel,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub struct TerminalPaletteIndex(u8);
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct TerminalClipboardQuery {
48 selection: Option<u8>,
49 terminator: InputEndType,
50}
51
52impl TerminalClipboardQuery {
53 const VALID_SELECTIONS: &'static [u8] = b"cpqs01234567";
54
55 #[must_use]
57 pub fn new(selection: &str, terminator: InputEndType) -> Self {
58 Self {
59 selection: selection
60 .bytes()
61 .find(|byte| Self::VALID_SELECTIONS.contains(byte)),
62 terminator,
63 }
64 }
65
66 #[must_use]
68 pub const fn selection(self) -> Option<u8> {
69 self.selection
70 }
71
72 #[must_use]
74 pub const fn terminator(self) -> InputEndType {
75 self.terminator
76 }
77}
78
79impl TerminalPaletteIndex {
80 #[must_use]
83 pub fn parse(value: &str) -> Option<Self> {
84 if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
85 return None;
86 }
87 value.parse::<u8>().ok().map(Self)
88 }
89
90 #[must_use]
92 pub const fn get(self) -> u8 {
93 self.0
94 }
95}
96
97impl From<u8> for TerminalPaletteIndex {
98 fn from(value: u8) -> Self {
99 Self(value)
100 }
101}
102
103impl TerminalPassthrough {
104 #[must_use]
106 pub fn raw(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
107 Self {
108 kind: TerminalPassthroughKind::Raw,
109 cursor_x,
110 cursor_y,
111 palette_index: None,
112 clipboard_query: None,
113 payload: Arc::from(payload.into()),
114 }
115 }
116
117 #[must_use]
119 pub fn clipboard(payload: impl Into<Vec<u8>>) -> Self {
120 Self {
121 kind: TerminalPassthroughKind::Clipboard,
122 cursor_x: 0,
123 cursor_y: 0,
124 palette_index: None,
125 clipboard_query: None,
126 payload: Arc::from(payload.into()),
127 }
128 }
129
130 #[must_use]
133 pub fn clipboard_query(query: TerminalClipboardQuery, payload: impl Into<Vec<u8>>) -> Self {
134 Self {
135 kind: TerminalPassthroughKind::Clipboard,
140 cursor_x: 0,
141 cursor_y: 0,
142 palette_index: None,
143 clipboard_query: Some(query),
144 payload: Arc::from(payload.into()),
145 }
146 }
147
148 #[must_use]
153 pub fn palette_query(index: TerminalPaletteIndex) -> Self {
154 let payload = format!("\x1b]4;{};?\x1b\\", index.get()).into_bytes();
155 Self {
156 kind: TerminalPassthroughKind::PaletteQuery,
157 cursor_x: 0,
158 cursor_y: 0,
159 palette_index: Some(index),
160 clipboard_query: None,
161 payload: Arc::from(payload),
162 }
163 }
164
165 #[must_use]
167 pub fn kitty_graphics(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
168 Self {
169 kind: TerminalPassthroughKind::KittyGraphics,
170 cursor_x,
171 cursor_y,
172 palette_index: None,
173 clipboard_query: None,
174 payload: Arc::from(payload.into()),
175 }
176 }
177
178 #[must_use]
180 pub fn sixel(cursor_x: u32, cursor_y: u32, payload: impl Into<Vec<u8>>) -> Self {
181 Self {
182 kind: TerminalPassthroughKind::Sixel,
183 cursor_x,
184 cursor_y,
185 palette_index: None,
186 clipboard_query: None,
187 payload: Arc::from(payload.into()),
188 }
189 }
190
191 #[must_use]
193 pub const fn kind(&self) -> TerminalPassthroughKind {
194 self.kind
195 }
196
197 #[must_use]
199 pub const fn cursor_x(&self) -> u32 {
200 self.cursor_x
201 }
202
203 #[must_use]
205 pub const fn cursor_y(&self) -> u32 {
206 self.cursor_y
207 }
208
209 #[must_use]
211 pub fn payload(&self) -> &[u8] {
212 &self.payload
213 }
214
215 #[must_use]
217 pub const fn palette_query_index(&self) -> Option<TerminalPaletteIndex> {
218 self.palette_index
219 }
220
221 #[must_use]
223 pub const fn clipboard_query_metadata(&self) -> Option<TerminalClipboardQuery> {
224 self.clipboard_query
225 }
226
227 #[must_use]
229 pub fn render_sequence(&self) -> Vec<u8> {
230 if self.clipboard_query.is_some() {
233 return Vec::new();
234 }
235 match self.kind {
236 TerminalPassthroughKind::Raw => self.payload.to_vec(),
237 TerminalPassthroughKind::Clipboard => self.payload.to_vec(),
238 TerminalPassthroughKind::PaletteQuery => self.payload.to_vec(),
239 TerminalPassthroughKind::KittyGraphics => {
240 let mut sequence = Vec::with_capacity(self.payload.len() + 4);
241 sequence.extend_from_slice(b"\x1b_");
242 sequence.extend_from_slice(&self.payload);
243 sequence.extend_from_slice(b"\x1b\\");
244 sequence
245 }
246 TerminalPassthroughKind::Sixel => {
247 let mut sequence = Vec::with_capacity(self.payload.len() + 4);
248 sequence.extend_from_slice(b"\x1bP");
249 sequence.extend_from_slice(&self.payload);
250 sequence.extend_from_slice(b"\x1b\\");
251 sequence
252 }
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::{TerminalClipboardQuery, TerminalPaletteIndex, TerminalPassthrough};
260 use crate::input::InputEndType;
261
262 #[test]
263 fn renders_kitty_apc_sequence() {
264 let passthrough = TerminalPassthrough::kitty_graphics(0, 0, b"Gf=100;AAAA".to_vec());
265
266 assert_eq!(passthrough.render_sequence(), b"\x1b_Gf=100;AAAA\x1b\\");
267 }
268
269 #[test]
270 fn renders_raw_sequence_verbatim() {
271 let passthrough = TerminalPassthrough::raw(0, 0, b"\x1b]52;c;QQ==\x1b\\".to_vec());
272
273 assert_eq!(passthrough.render_sequence(), b"\x1b]52;c;QQ==\x1b\\");
274 }
275
276 #[test]
277 fn renders_clipboard_sequence_verbatim() {
278 let passthrough = TerminalPassthrough::clipboard(b"\x1b]52;c;QQ==\x07".to_vec());
279
280 assert_eq!(passthrough.render_sequence(), b"\x1b]52;c;QQ==\x07");
281 }
282
283 #[test]
284 fn clipboard_query_is_typed_bounded_and_never_generically_rendered() {
285 let query = TerminalClipboardQuery::new("zzpc", InputEndType::St);
286 assert_eq!(query.selection(), Some(b'p'));
287 assert_eq!(query.terminator(), InputEndType::St);
288
289 let passthrough =
290 TerminalPassthrough::clipboard_query(query, b"\x1b]52;zzpc;?\x1b\\".to_vec());
291 assert_eq!(
292 passthrough.kind(),
293 super::TerminalPassthroughKind::Clipboard
294 );
295 assert_eq!(passthrough.clipboard_query_metadata(), Some(query));
296 assert_eq!(passthrough.payload(), b"\x1b]52;zzpc;?\x1b\\");
297 assert!(passthrough.render_sequence().is_empty());
298
299 let invalid = TerminalClipboardQuery::new("xyz", InputEndType::Bel);
300 assert_eq!(invalid.selection(), None);
301 }
302
303 #[test]
304 fn palette_query_is_bounded_typed_and_canonical() {
305 assert_eq!(
306 TerminalPaletteIndex::parse("0").map(TerminalPaletteIndex::get),
307 Some(0)
308 );
309 assert_eq!(
310 TerminalPaletteIndex::parse("255").map(TerminalPaletteIndex::get),
311 Some(255)
312 );
313 assert_eq!(TerminalPaletteIndex::parse("256"), None);
314 assert_eq!(TerminalPaletteIndex::parse("-1"), None);
315
316 let query = TerminalPassthrough::palette_query(TerminalPaletteIndex::from(255));
317 assert_eq!(query.render_sequence(), b"\x1b]4;255;?\x1b\\");
318 assert_eq!(
319 query.palette_query_index(),
320 Some(TerminalPaletteIndex::from(255))
321 );
322 }
323
324 #[test]
325 fn renders_sixel_dcs_sequence() {
326 let passthrough = TerminalPassthrough::sixel(0, 0, b"q#0!10~".to_vec());
327
328 assert_eq!(passthrough.render_sequence(), b"\x1bPq#0!10~\x1b\\");
329 }
330}