Skip to main content

kitty_graphics_protocol/
response.rs

1//! Response parsing for the Kitty graphics protocol
2
3use crate::error::{Error, Result};
4
5/// Response from the terminal
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Response {
8    /// Image ID (if applicable)
9    pub image_id: Option<u32>,
10    /// Image number (if applicable)
11    pub image_number: Option<u32>,
12    /// Placement ID (if applicable)
13    pub placement_id: Option<u32>,
14    /// Whether the operation was successful
15    pub success: bool,
16    /// Error message (if failed)
17    pub error: Option<String>,
18}
19
20impl Response {
21    /// Parse a response from the terminal
22    pub fn parse(data: &[u8]) -> Result<Self> {
23        // Expected format: <ESC>_Gi=<id>;OK<ESC>\ or <ESC>_Gi=<id>;ERROR:message<ESC>\
24        // Also: <ESC>_Gi=<id>,p=<placement_id>;OK<ESC>\
25        // And: <ESC>_Gi=<id>,I=<number>;OK<ESC>\
26
27        // Check for APC start
28        if data.len() < 6 {
29            return Err(Error::InvalidResponse(
30                String::from_utf8_lossy(data).into_owned(),
31            ));
32        }
33
34        if data[0] != crate::ESC || data[1] != b'_' || data[2] != b'G' {
35            return Err(Error::InvalidResponse(
36                String::from_utf8_lossy(data).into_owned(),
37            ));
38        }
39
40        // Find the semicolon separator
41        let semicolon_pos = data
42            .iter()
43            .position(|&b| b == b';')
44            .ok_or_else(|| Error::InvalidResponse(String::from_utf8_lossy(data).into_owned()))?;
45
46        // Parse control data (between G and ;)
47        let control = &data[3..semicolon_pos];
48        let control_str = std::str::from_utf8(control).map_err(Error::from)?;
49
50        // Parse the message (after semicolon until ESC \)
51        let end_pos = data
52            .iter()
53            .rposition(|&b| b == crate::ESC)
54            .ok_or_else(|| Error::InvalidResponse(String::from_utf8_lossy(data).into_owned()))?;
55
56        let message = &data[semicolon_pos + 1..end_pos];
57        let message_str = std::str::from_utf8(message).map_err(Error::from)?;
58
59        // Parse control fields
60        let mut image_id = None;
61        let mut image_number = None;
62        let mut placement_id = None;
63
64        for part in control_str.split(',') {
65            let parts: Vec<&str> = part.splitn(2, '=').collect();
66            if parts.len() == 2 {
67                match parts[0] {
68                    "i" => image_id = parts[1].parse().ok(),
69                    "I" => image_number = parts[1].parse().ok(),
70                    "p" => placement_id = parts[1].parse().ok(),
71                    _ => {}
72                }
73            }
74        }
75
76        // Parse message
77        let (success, error) = if message_str == "OK" {
78            (true, None)
79        } else if let Some(err_msg) = message_str.strip_prefix("ENOENT:") {
80            (false, Some(format!("Not found: {err_msg}")))
81        } else if let Some(err_msg) = message_str.strip_prefix("EINVAL:") {
82            (false, Some(format!("Invalid argument: {err_msg}")))
83        } else if let Some(err_msg) = message_str.strip_prefix("EIO:") {
84            (false, Some(format!("IO error: {err_msg}")))
85        } else if let Some(err_msg) = message_str.strip_prefix("ETOODEEP:") {
86            (false, Some(format!("Chain too deep: {err_msg}")))
87        } else if let Some(err_msg) = message_str.strip_prefix("ECYCLE:") {
88            (false, Some(format!("Cycle detected: {err_msg}")))
89        } else if let Some(err_msg) = message_str.strip_prefix("ENOPARENT:") {
90            (false, Some(format!("Parent not found: {err_msg}")))
91        } else {
92            (false, Some(message_str.to_string()))
93        };
94
95        Ok(Response {
96            image_id,
97            image_number,
98            placement_id,
99            success,
100            error,
101        })
102    }
103
104    /// Check if this is a success response
105    pub fn is_ok(&self) -> bool {
106        self.success
107    }
108
109    /// Check if this is an error response
110    pub fn is_error(&self) -> bool {
111        !self.success
112    }
113
114    /// Get the error message if this is an error
115    pub fn error_message(&self) -> Option<&str> {
116        self.error.as_deref()
117    }
118}
119
120impl std::fmt::Display for Response {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        if self.success {
123            write!(f, "OK")?;
124            if let Some(id) = self.image_id {
125                write!(f, " (image_id={}", id)?;
126                if let Some(pid) = self.placement_id {
127                    write!(f, ", placement_id={}", pid)?;
128                }
129                write!(f, ")")?;
130            }
131        } else if let Some(err) = &self.error {
132            write!(f, "ERROR: {}", err)?;
133        }
134        Ok(())
135    }
136}
137
138/// Common error codes returned by the terminal
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
140pub enum ErrorCode {
141    /// Not found (ENOENT)
142    NotFound,
143    /// Invalid argument (EINVAL)
144    InvalidArgument,
145    /// IO error (EIO)
146    IoError,
147    /// Chain too deep (ETOODEEP)
148    TooDeep,
149    /// Cycle detected (ECYCLE)
150    Cycle,
151    /// Parent not found (ENOPARENT)
152    NoParent,
153    /// Unknown error
154    Unknown,
155}
156
157impl ErrorCode {
158    /// Parse an error code from a response
159    pub fn from_message(msg: &str) -> Self {
160        if msg.starts_with("ENOENT") {
161            Self::NotFound
162        } else if msg.starts_with("EINVAL") {
163            Self::InvalidArgument
164        } else if msg.starts_with("EIO") {
165            Self::IoError
166        } else if msg.starts_with("ETOODEEP") {
167            Self::TooDeep
168        } else if msg.starts_with("ECYCLE") {
169            Self::Cycle
170        } else if msg.starts_with("ENOPARENT") {
171            Self::NoParent
172        } else {
173            Self::Unknown
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_parse_ok_response() {
184        let data = b"\x1b_Gi=42;OK\x1b\\";
185        let resp = Response::parse(data).unwrap();
186        assert!(resp.is_ok());
187        assert_eq!(resp.image_id, Some(42));
188    }
189
190    #[test]
191    fn test_parse_ok_response_with_placement() {
192        let data = b"\x1b_Gi=42,p=7;OK\x1b\\";
193        let resp = Response::parse(data).unwrap();
194        assert!(resp.is_ok());
195        assert_eq!(resp.image_id, Some(42));
196        assert_eq!(resp.placement_id, Some(7));
197    }
198
199    #[test]
200    fn test_parse_error_response() {
201        let data = b"\x1b_Gi=42;ENOENT:Image not found\x1b\\";
202        let resp = Response::parse(data).unwrap();
203        assert!(resp.is_error());
204        assert_eq!(resp.image_id, Some(42));
205        assert!(resp.error.unwrap().contains("Not found"));
206    }
207
208    #[test]
209    fn test_parse_response_with_image_number() {
210        let data = b"\x1b_Gi=99,I=13;OK\x1b\\";
211        let resp = Response::parse(data).unwrap();
212        assert!(resp.is_ok());
213        assert_eq!(resp.image_id, Some(99));
214        assert_eq!(resp.image_number, Some(13));
215    }
216}