Skip to main content

kitty_graphics_protocol/
command.rs

1//! Command building and serialization for the Kitty graphics protocol
2
3use crate::error::{Error, Result};
4use crate::types::*;
5use crate::{APC_END, APC_START, GRAPHICS_PREFIX, MAX_CHUNK_SIZE};
6use base64::{Engine, engine::general_purpose::STANDARD};
7use std::fmt;
8
9/// Builder for constructing graphics protocol commands
10#[derive(Debug, Clone, Default)]
11pub struct CommandBuilder {
12    /// Action to perform
13    action: Option<Action>,
14    /// Image format
15    format: Option<ImageFormat>,
16    /// Transmission medium
17    medium: Option<TransmissionMedium>,
18    /// Image width in pixels
19    width: Option<u32>,
20    /// Image height in pixels
21    height: Option<u32>,
22    /// Image ID (0-4294967295, must not be zero for some operations)
23    image_id: Option<u32>,
24    /// Image number (alternative to image_id)
25    image_number: Option<u32>,
26    /// Placement ID
27    placement_id: Option<u32>,
28    /// More data flag (0 = last chunk, 1 = more chunks)
29    more_data: Option<bool>,
30    /// Compression algorithm
31    compression: Option<Compression>,
32    /// Quiet mode (1 = suppress OK, 2 = suppress errors)
33    quiet: Option<u8>,
34    /// Source rectangle X offset
35    source_x: Option<u32>,
36    /// Source rectangle Y offset
37    source_y: Option<u32>,
38    /// Source rectangle width
39    source_width: Option<u32>,
40    /// Source rectangle height
41    source_height: Option<u32>,
42    /// Cell offset X (within current cell)
43    cell_offset_x: Option<u32>,
44    /// Cell offset Y (within current cell)
45    cell_offset_y: Option<u32>,
46    /// Number of columns to display
47    columns: Option<u32>,
48    /// Number of rows to display
49    rows: Option<u32>,
50    /// Z-index for stacking order
51    z_index: Option<i32>,
52    /// Cursor movement policy
53    cursor_policy: Option<CursorPolicy>,
54    /// Delete target
55    delete_target: Option<DeleteTarget>,
56    /// File path or shared memory name
57    path: Option<String>,
58    /// Data size for file/shared memory
59    data_size: Option<usize>,
60    /// Data offset for file/shared memory
61    data_offset: Option<usize>,
62    /// Unicode placeholder mode
63    unicode_placeholder: Option<UnicodePlaceholder>,
64    /// Parent image ID for relative placement
65    parent_image_id: Option<u32>,
66    /// Parent placement ID for relative placement
67    parent_placement_id: Option<u32>,
68    /// Horizontal offset for relative placement
69    relative_h_offset: Option<i32>,
70    /// Vertical offset for relative placement
71    relative_v_offset: Option<i32>,
72    /// Animation control
73    animation_control: Option<AnimationControl>,
74    /// Animation frame number (for various operations)
75    frame_number: Option<u32>,
76    /// Frame gap in milliseconds
77    frame_gap: Option<i32>,
78    /// Loop count (0 = ignored, 1 = infinite)
79    loop_count: Option<u32>,
80    /// Background color for frame (RGBA)
81    background_color: Option<u32>,
82    /// Reference frame for composition
83    ref_frame: Option<u32>,
84    /// Frame composition parameters
85    composition: Option<FrameComposition>,
86}
87
88impl CommandBuilder {
89    /// Create a new command builder
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Set the action
95    pub fn action(mut self, action: Action) -> Self {
96        self.action = Some(action);
97        self
98    }
99
100    /// Set the image format
101    pub fn format(mut self, format: ImageFormat) -> Self {
102        self.format = Some(format);
103        self
104    }
105
106    /// Set the transmission medium
107    pub fn medium(mut self, medium: TransmissionMedium) -> Self {
108        self.medium = Some(medium);
109        self
110    }
111
112    /// Set the image dimensions (width, height) in pixels
113    pub fn dimensions(mut self, width: u32, height: u32) -> Self {
114        self.width = Some(width);
115        self.height = Some(height);
116        self
117    }
118
119    /// Set the image ID
120    pub fn image_id(mut self, id: u32) -> Self {
121        self.image_id = Some(id);
122        self
123    }
124
125    /// Set the image number (alternative to image ID)
126    pub fn image_number(mut self, number: u32) -> Self {
127        self.image_number = Some(number);
128        self
129    }
130
131    /// Set the placement ID
132    pub fn placement_id(mut self, id: u32) -> Self {
133        self.placement_id = Some(id);
134        self
135    }
136
137    /// Set the more data flag
138    pub fn more_data(mut self, more: bool) -> Self {
139        self.more_data = Some(more);
140        self
141    }
142
143    /// Set compression
144    pub fn compression(mut self, compression: Compression) -> Self {
145        self.compression = Some(compression);
146        self
147    }
148
149    /// Set quiet mode (1 = suppress OK, 2 = suppress errors)
150    pub fn quiet(mut self, mode: u8) -> Self {
151        self.quiet = Some(mode);
152        self
153    }
154
155    /// Set the source rectangle (x, y, width, height)
156    pub fn source_rect(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
157        self.source_x = Some(x);
158        self.source_y = Some(y);
159        self.source_width = Some(width);
160        self.source_height = Some(height);
161        self
162    }
163
164    /// Set cell offset (X, Y) within the current cell
165    pub fn cell_offset(mut self, x: u32, y: u32) -> Self {
166        self.cell_offset_x = Some(x);
167        self.cell_offset_y = Some(y);
168        self
169    }
170
171    /// Set display area in columns and rows
172    pub fn display_area(mut self, columns: u32, rows: u32) -> Self {
173        self.columns = Some(columns);
174        self.rows = Some(rows);
175        self
176    }
177
178    /// Set z-index
179    pub fn z_index(mut self, z: i32) -> Self {
180        self.z_index = Some(z);
181        self
182    }
183
184    /// Set cursor policy
185    pub fn cursor_policy(mut self, policy: CursorPolicy) -> Self {
186        self.cursor_policy = Some(policy);
187        self
188    }
189
190    /// Set delete target
191    pub fn delete_target(mut self, target: DeleteTarget) -> Self {
192        self.delete_target = Some(target);
193        self
194    }
195
196    /// Set file path or shared memory name
197    pub fn path(mut self, path: impl Into<String>) -> Self {
198        self.path = Some(path.into());
199        self
200    }
201
202    /// Set data size and offset for file/shared memory
203    pub fn data_range(mut self, size: usize, offset: usize) -> Self {
204        self.data_size = Some(size);
205        self.data_offset = Some(offset);
206        self
207    }
208
209    /// Set unicode placeholder mode
210    pub fn unicode_placeholder(mut self, columns: u16, rows: u16) -> Self {
211        self.unicode_placeholder = Some(UnicodePlaceholder { columns, rows });
212        self
213    }
214
215    /// Set parent for relative placement
216    pub fn parent(mut self, image_id: u32, placement_id: u32) -> Self {
217        self.parent_image_id = Some(image_id);
218        self.parent_placement_id = Some(placement_id);
219        self
220    }
221
222    /// Set relative offset for relative placement
223    pub fn relative_offset(mut self, h: i32, v: i32) -> Self {
224        self.relative_h_offset = Some(h);
225        self.relative_v_offset = Some(v);
226        self
227    }
228
229    /// Set animation control
230    pub fn animation_control(mut self, control: AnimationControl) -> Self {
231        self.animation_control = Some(control);
232        self
233    }
234
235    /// Set frame number
236    pub fn frame_number(mut self, frame: u32) -> Self {
237        self.frame_number = Some(frame);
238        self
239    }
240
241    /// Set frame gap in milliseconds (negative = gapless frame)
242    pub fn frame_gap(mut self, gap_ms: i32) -> Self {
243        self.frame_gap = Some(gap_ms);
244        self
245    }
246
247    /// Set loop count (0 = ignored, 1 = infinite)
248    pub fn loop_count(mut self, count: u32) -> Self {
249        self.loop_count = Some(count);
250        self
251    }
252
253    /// Set background color for frame (RGBA)
254    pub fn background_color(mut self, color: u32) -> Self {
255        self.background_color = Some(color);
256        self
257    }
258
259    /// Set reference frame for composition
260    pub fn ref_frame(mut self, frame: u32) -> Self {
261        self.ref_frame = Some(frame);
262        self
263    }
264
265    /// Set frame composition parameters
266    pub fn composition(mut self, comp: FrameComposition) -> Self {
267        self.composition = Some(comp);
268        self
269    }
270
271    /// Build the command
272    pub fn build(self) -> Command {
273        Command { inner: self }
274    }
275}
276
277/// A graphics protocol command ready for serialization
278#[derive(Debug, Clone)]
279pub struct Command {
280    inner: CommandBuilder,
281}
282
283impl Command {
284    /// Create a new command builder
285    pub fn builder() -> CommandBuilder {
286        CommandBuilder::new()
287    }
288
289    /// Build the control data string (key=value pairs)
290    fn build_control_data(&self) -> String {
291        let mut parts = Vec::new();
292
293        // Action (a)
294        if let Some(action) = &self.inner.action {
295            parts.push(format!("a={action}"));
296        }
297
298        // Format (f)
299        if let Some(format) = &self.inner.format {
300            parts.push(format!("f={format}"));
301        }
302
303        // Transmission medium (t)
304        if let Some(medium) = &self.inner.medium {
305            parts.push(format!("t={medium}"));
306        }
307
308        // Image dimensions (s, v)
309        if let Some(width) = self.inner.width {
310            parts.push(format!("s={width}"));
311        }
312        if let Some(height) = self.inner.height {
313            parts.push(format!("v={height}"));
314        }
315
316        // Image ID (i) or Image Number (I)
317        if let Some(id) = self.inner.image_id {
318            parts.push(format!("i={id}"));
319        } else if let Some(num) = self.inner.image_number {
320            parts.push(format!("I={num}"));
321        }
322
323        // Placement ID (p)
324        if let Some(id) = self.inner.placement_id {
325            parts.push(format!("p={id}"));
326        }
327
328        // More data flag (m)
329        if let Some(more) = self.inner.more_data {
330            parts.push(format!("m={}", if more { 1 } else { 0 }));
331        }
332
333        // Compression (o)
334        if let Some(comp) = &self.inner.compression {
335            parts.push(format!("o={comp}"));
336        }
337
338        // Quiet mode (q)
339        if let Some(quiet) = self.inner.quiet {
340            parts.push(format!("q={quiet}"));
341        }
342
343        // Source rectangle (x, y, w, h)
344        if let Some(x) = self.inner.source_x {
345            parts.push(format!("x={x}"));
346        }
347        if let Some(y) = self.inner.source_y {
348            parts.push(format!("y={y}"));
349        }
350        if let Some(w) = self.inner.source_width {
351            parts.push(format!("w={w}"));
352        }
353        if let Some(h) = self.inner.source_height {
354            parts.push(format!("h={h}"));
355        }
356
357        // Cell offset (X, Y)
358        if let Some(x) = self.inner.cell_offset_x {
359            parts.push(format!("X={x}"));
360        }
361        if let Some(y) = self.inner.cell_offset_y {
362            parts.push(format!("Y={y}"));
363        }
364
365        // Display area (c, r)
366        if let Some(cols) = self.inner.columns {
367            parts.push(format!("c={cols}"));
368        }
369        if let Some(rows) = self.inner.rows {
370            parts.push(format!("r={rows}"));
371        }
372
373        // Z-index (z)
374        if let Some(z) = self.inner.z_index {
375            parts.push(format!("z={z}"));
376        }
377
378        // Cursor policy (C)
379        if let Some(policy) = &self.inner.cursor_policy
380            && matches!(policy, CursorPolicy::NoMove)
381        {
382            parts.push(format!("C={policy}"));
383        }
384
385        // Delete target (d)
386        if let Some(target) = &self.inner.delete_target {
387            parts.push(format!("d={}", target.code()));
388        }
389
390        // File path or shared memory name
391        if let Some(_path) = &self.inner.path {
392            // The path needs to be encoded in the payload, not control data
393            // We'll handle this separately in serialize_with_path
394        }
395
396        // Data size (S) and offset (O)
397        if let Some(size) = self.inner.data_size {
398            parts.push(format!("S={size}"));
399        }
400        if let Some(offset) = self.inner.data_offset {
401            parts.push(format!("O={offset}"));
402        }
403
404        // Unicode placeholder (U)
405        if self.inner.unicode_placeholder.is_some() {
406            parts.push("U=1".to_string());
407        }
408
409        // Parent for relative placement (P, Q)
410        if let Some(id) = self.inner.parent_image_id {
411            parts.push(format!("P={id}"));
412        }
413        if let Some(id) = self.inner.parent_placement_id {
414            parts.push(format!("Q={id}"));
415        }
416
417        // Relative offset (H, V)
418        if let Some(h) = self.inner.relative_h_offset {
419            parts.push(format!("H={h}"));
420        }
421        if let Some(v) = self.inner.relative_v_offset {
422            parts.push(format!("V={v}"));
423        }
424
425        // Animation control (s) - note: same letter as width, context matters
426        // For animation control, this is set via action=a
427        // The animation state is controlled by s=1/2/3
428        if let Some(control) = &self.inner.animation_control {
429            parts.push(format!("s={control}"));
430        }
431
432        // Frame number for various operations
433        // For animation frame: c=frame number
434        // For frame edit: r=frame number
435        if let Some(frame) = self.inner.frame_number {
436            // Context determines which key to use
437            // For now, use 'c' for frame selection
438            parts.push(format!("c={frame}"));
439        }
440
441        // Frame gap (z) - note: same letter as z-index
442        // When action=f, z means frame gap
443        if let Some(gap) = self.inner.frame_gap
444            && gap != 0
445        {
446            parts.push(format!("z={gap}"));
447        }
448
449        // Loop count (v) - note: same letter as height
450        // When action=a, v means loop count
451        if let Some(count) = self.inner.loop_count
452            && count > 0
453        {
454            parts.push(format!("v={count}"));
455        }
456
457        // Background color (Y)
458        if let Some(color) = self.inner.background_color {
459            parts.push(format!("Y={color}"));
460        }
461
462        // Reference frame (c) for frame composition
463        // Already handled above as frame_number
464
465        parts.join(",")
466    }
467
468    /// Serialize the command to an escape sequence string
469    pub fn serialize(&self, data: &[u8]) -> Result<String> {
470        let control = self.build_control_data();
471        let encoded = STANDARD.encode(data);
472
473        let mut result = Vec::new();
474
475        // Start sequence
476        result.extend_from_slice(APC_START);
477        result.extend_from_slice(GRAPHICS_PREFIX.as_bytes());
478
479        // Control data
480        result.extend_from_slice(control.as_bytes());
481
482        // Payload separator and payload
483        result.push(b';');
484        result.extend_from_slice(encoded.as_bytes());
485
486        // End sequence
487        result.extend_from_slice(APC_END);
488
489        String::from_utf8(result).map_err(Error::from)
490    }
491
492    /// Serialize the command to bytes
493    pub fn serialize_bytes(&self, data: &[u8]) -> Result<Vec<u8>> {
494        let control = self.build_control_data();
495        let encoded = STANDARD.encode(data);
496
497        let mut result = Vec::new();
498
499        result.extend_from_slice(APC_START);
500        result.extend_from_slice(GRAPHICS_PREFIX.as_bytes());
501        result.extend_from_slice(control.as_bytes());
502        result.push(b';');
503        result.extend_from_slice(encoded.as_bytes());
504        result.extend_from_slice(APC_END);
505
506        Ok(result)
507    }
508
509    /// Serialize command in chunks for large data
510    /// Returns an iterator of escape sequences
511    pub fn serialize_chunked(&self, data: &[u8]) -> Result<ChunkedSerializer> {
512        // First, encode all data to base64
513        let encoded = STANDARD.encode(data);
514
515        // Calculate chunk size that's a multiple of 4
516        let chunk_size = (MAX_CHUNK_SIZE / 4) * 4;
517
518        Ok(ChunkedSerializer {
519            control: self.build_control_data(),
520            encoded,
521            chunk_size,
522            offset: 0,
523            is_first: true,
524        })
525    }
526
527    /// Serialize a command with a path (for file/shared memory transmission)
528    pub fn serialize_with_path(&self) -> Result<String> {
529        let control = self.build_control_data();
530        let path = self
531            .inner
532            .path
533            .as_ref()
534            .ok_or(Error::MissingField("path"))?;
535        let encoded_path = STANDARD.encode(path.as_bytes());
536
537        let mut result = Vec::new();
538
539        result.extend_from_slice(APC_START);
540        result.extend_from_slice(GRAPHICS_PREFIX.as_bytes());
541        result.extend_from_slice(control.as_bytes());
542        result.push(b';');
543        result.extend_from_slice(encoded_path.as_bytes());
544        result.extend_from_slice(APC_END);
545
546        String::from_utf8(result).map_err(Error::from)
547    }
548}
549
550/// Iterator for chunked serialization of large data
551pub struct ChunkedSerializer {
552    control: String,
553    encoded: String,
554    chunk_size: usize,
555    offset: usize,
556    is_first: bool,
557}
558
559impl ChunkedSerializer {
560    /// Get the total number of chunks
561    pub fn total_chunks(&self) -> usize {
562        self.encoded.len().div_ceil(self.chunk_size)
563    }
564
565    /// Check if there are more chunks
566    pub fn has_more(&self) -> bool {
567        self.offset < self.encoded.len()
568    }
569}
570
571impl Iterator for ChunkedSerializer {
572    type Item = String;
573
574    fn next(&mut self) -> Option<Self::Item> {
575        if self.offset >= self.encoded.len() {
576            return None;
577        }
578
579        let end = (self.offset + self.chunk_size).min(self.encoded.len());
580        let chunk = &self.encoded[self.offset..end];
581        let is_last = end >= self.encoded.len();
582
583        let mut result = Vec::new();
584        result.extend_from_slice(APC_START);
585        result.extend_from_slice(GRAPHICS_PREFIX.as_bytes());
586
587        if self.is_first {
588            // First chunk includes all control data
589            result.extend_from_slice(self.control.as_bytes());
590            result.push(b',');
591            self.is_first = false;
592        }
593
594        // m=1 for more data, m=0 for last chunk
595        result.extend_from_slice(format!("m={}", if is_last { 0 } else { 1 }).as_bytes());
596        result.push(b';');
597        result.extend_from_slice(chunk.as_bytes());
598        result.extend_from_slice(APC_END);
599
600        self.offset = end;
601
602        String::from_utf8(result).ok()
603    }
604}
605
606impl fmt::Display for Command {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        write!(f, "Command({})", self.build_control_data())
609    }
610}
611
612/// Convenience functions for common operations
613impl Command {
614    /// Create a command to query protocol support
615    pub fn query_support() -> Self {
616        Self::builder().action(Action::Query).quiet(2).build()
617    }
618
619    /// Create a command to transmit and display a PNG image
620    pub fn transmit_png(data: &[u8]) -> Result<Vec<String>> {
621        let cmd = Self::builder()
622            .action(Action::TransmitAndDisplay)
623            .format(ImageFormat::Png)
624            .quiet(2)
625            .build();
626
627        let chunks: Vec<String> = cmd.serialize_chunked(data)?.collect();
628        Ok(chunks)
629    }
630
631    /// Create a command to transmit and display raw RGBA data
632    pub fn transmit_rgba(data: &[u8], width: u32, height: u32) -> Result<Vec<String>> {
633        let expected_size = (width * height * 4) as usize;
634        if data.len() != expected_size {
635            return Err(Error::InvalidDimensions { width, height });
636        }
637
638        let cmd = Self::builder()
639            .action(Action::TransmitAndDisplay)
640            .format(ImageFormat::Rgba)
641            .dimensions(width, height)
642            .quiet(2)
643            .build();
644
645        let chunks: Vec<String> = cmd.serialize_chunked(data)?.collect();
646        Ok(chunks)
647    }
648
649    /// Create a command to transmit and display raw RGB data
650    pub fn transmit_rgb(data: &[u8], width: u32, height: u32) -> Result<Vec<String>> {
651        let expected_size = (width * height * 3) as usize;
652        if data.len() != expected_size {
653            return Err(Error::InvalidDimensions { width, height });
654        }
655
656        let cmd = Self::builder()
657            .action(Action::TransmitAndDisplay)
658            .format(ImageFormat::Rgb)
659            .dimensions(width, height)
660            .quiet(2)
661            .build();
662
663        let chunks: Vec<String> = cmd.serialize_chunked(data)?.collect();
664        Ok(chunks)
665    }
666
667    /// Create a command to delete all visible placements
668    pub fn delete_all() -> Self {
669        Self::builder()
670            .action(Action::Delete)
671            .delete_target(DeleteTarget::All)
672            .build()
673    }
674
675    /// Create a command to delete an image by ID
676    pub fn delete_by_id(image_id: u32) -> Self {
677        Self::builder()
678            .action(Action::Delete)
679            .delete_target(DeleteTarget::ById { free_data: true })
680            .image_id(image_id)
681            .build()
682    }
683
684    /// Create a command to place a previously transmitted image
685    pub fn place(image_id: u32, columns: u32, rows: u32) -> Self {
686        Self::builder()
687            .action(Action::Place)
688            .image_id(image_id)
689            .display_area(columns, rows)
690            .build()
691    }
692}