kitty_graphics_protocol/
terminal.rs1use crate::error::{Error, Result};
4use std::io::{self, Read, Write};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct WindowSize {
9 pub rows: u16,
11 pub cols: u16,
13 pub width: u16,
15 pub height: u16,
17}
18
19impl WindowSize {
20 pub fn cell_width(&self) -> u16 {
22 if self.cols > 0 {
23 self.width / self.cols
24 } else {
25 0
26 }
27 }
28
29 pub fn cell_height(&self) -> u16 {
31 if self.rows > 0 {
32 self.height / self.rows
33 } else {
34 0
35 }
36 }
37
38 pub fn cells_for_image(&self, img_width: u32, img_height: u32) -> (u32, u32) {
40 let cell_w = self.cell_width() as u32;
41 let cell_h = self.cell_height() as u32;
42
43 if cell_w == 0 || cell_h == 0 {
44 return (0, 0);
45 }
46
47 let cols = img_width.div_ceil(cell_w);
48 let rows = img_height.div_ceil(cell_h);
49
50 (cols, rows)
51 }
52}
53
54#[cfg(unix)]
55mod unix {
56 use super::*;
57 use libc::{STDOUT_FILENO, TIOCGWINSZ, ioctl, winsize};
58
59 pub fn get_window_size() -> Result<WindowSize> {
61 unsafe {
62 let mut ws: winsize = std::mem::zeroed();
63 let result = ioctl(STDOUT_FILENO, TIOCGWINSZ, &mut ws);
64
65 if result == -1 {
66 return Err(Error::Io(io::Error::last_os_error()));
67 }
68
69 Ok(WindowSize {
70 rows: ws.ws_row,
71 cols: ws.ws_col,
72 width: ws.ws_xpixel,
73 height: ws.ws_ypixel,
74 })
75 }
76 }
77}
78
79#[cfg(not(unix))]
80mod other {
81 use super::*;
82
83 pub fn get_window_size() -> Result<WindowSize> {
85 Err(Error::protocol(
86 "get_window_size is only supported on Unix systems",
87 ))
88 }
89}
90
91#[cfg(not(unix))]
92pub use other::get_window_size;
93#[cfg(unix)]
94pub use unix::get_window_size;
95
96pub fn query_window_size() -> Result<WindowSize> {
99 let mut stdout = io::stdout();
100 let mut stdin = io::stdin();
101
102 #[cfg(unix)]
104 {
105 use std::os::unix::io::AsRawFd;
106 let fd = stdin.as_raw_fd();
107 let mut termios = std::mem::MaybeUninit::uninit();
108 if unsafe { libc::tcgetattr(fd, termios.as_mut_ptr()) } == 0 {
109 let termios = unsafe { termios.assume_init() };
110 let _ = unsafe { libc::tcsetattr(fd, libc::TCSAFLUSH, &termios) };
111 }
112 }
113
114 write!(stdout, "\x1b[14t")?;
116 stdout.flush()?;
117
118 let mut response = Vec::new();
120 let mut buf = [0u8; 1];
121
122 loop {
123 let n = stdin.read(&mut buf)?;
124 if n == 0 {
125 break;
126 }
127 response.push(buf[0]);
128 if buf[0] == b't' {
129 break;
130 }
131 if response.len() > 100 {
132 break; }
134 }
135
136 let response_str = String::from_utf8(response).map_err(Error::from)?;
138 parse_size_response(&response_str)
139}
140
141fn parse_size_response(response: &str) -> Result<WindowSize> {
142 if !response.starts_with("\x1b[4;") {
144 return Err(Error::InvalidResponse(response.to_string()));
145 }
146
147 let parts: Vec<&str> = response[4..].trim_end_matches('t').split(';').collect();
148 if parts.len() < 2 {
149 return Err(Error::InvalidResponse(response.to_string()));
150 }
151
152 let height: u16 = parts[0]
153 .parse()
154 .map_err(|_| Error::InvalidResponse(response.to_string()))?;
155 let width: u16 = parts[1]
156 .parse()
157 .map_err(|_| Error::InvalidResponse(response.to_string()))?;
158
159 let (rows, cols) = get_terminal_size_from_stty()?;
161
162 Ok(WindowSize {
163 rows,
164 cols,
165 width,
166 height,
167 })
168}
169
170#[cfg(unix)]
171fn get_terminal_size_from_stty() -> Result<(u16, u16)> {
172 use std::process::Command;
173
174 let output = Command::new("stty").arg("size").output()?;
175
176 if !output.status.success() {
177 return Err(Error::Io(io::Error::other("stty size failed")));
178 }
179
180 let size_str = String::from_utf8_lossy(&output.stdout);
181 let size_owned = size_str.into_owned();
182 let parts: Vec<&str> = size_owned.split_whitespace().collect();
183
184 if parts.len() < 2 {
185 return Err(Error::InvalidResponse(size_owned));
186 }
187
188 let rows: u16 = parts[0]
189 .parse()
190 .map_err(|_| Error::InvalidResponse(size_owned.clone()))?;
191 let cols: u16 = parts[1]
192 .parse()
193 .map_err(|_| Error::InvalidResponse(size_owned))?;
194
195 Ok((rows, cols))
196}
197
198#[cfg(not(unix))]
199fn get_terminal_size_from_stty() -> Result<(u16, u16)> {
200 Ok((24, 80))
202}
203
204pub fn check_protocol_support() -> Result<bool> {
206 #[cfg(unix)]
207 {
208 use std::os::unix::io::AsRawFd;
209
210 let stdin = io::stdin();
211 let fd = stdin.as_raw_fd();
212
213 if unsafe { libc::isatty(fd) } != 1 {
215 return Ok(true);
218 }
219
220 let mut stdout = io::stdout();
221
222 let mut original_termios: libc::termios = unsafe { std::mem::zeroed() };
224 if unsafe { libc::tcgetattr(fd, &mut original_termios) } != 0 {
225 return Ok(true);
227 }
228
229 let mut raw_termios = original_termios;
231 unsafe { libc::cfmakeraw(&mut raw_termios) };
232 if unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw_termios) } != 0 {
233 return Ok(true);
235 }
236
237 let _ = write!(stdout, "\x1b_Ga=q,i=31,s=1,v=1,f=24;AAAA\x1b\\");
240 let _ = stdout.flush();
241
242 let mut response = Vec::new();
244 let mut buf = [0u8; 256];
245
246 let start = std::time::Instant::now();
247
248 loop {
249 if start.elapsed() > std::time::Duration::from_millis(200) {
250 break;
251 }
252
253 let mut tv = libc::timeval {
255 tv_sec: 0,
256 tv_usec: 50_000, };
258
259 let mut read_fds: libc::fd_set = unsafe { std::mem::zeroed() };
261 unsafe { libc::FD_SET(fd, &mut read_fds) };
262
263 let ready = unsafe {
264 libc::select(
265 fd + 1,
266 &mut read_fds,
267 std::ptr::null_mut(),
268 std::ptr::null_mut(),
269 &mut tv,
270 )
271 };
272
273 if ready > 0 {
274 let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
275 if n > 0 {
276 response.extend_from_slice(&buf[..n as usize]);
277 if response.windows(2).any(|w| *w == [0x1b, b'\\']) {
279 break;
280 }
281 } else {
282 break;
283 }
284 }
285 }
286
287 unsafe { libc::tcsetattr(fd, libc::TCSANOW, &original_termios) };
289
290 let response_str = String::from_utf8_lossy(&response);
291
292 let has_apc = response.windows(3).any(|w| *w == [0x1b, b'_', b'G']);
295 let has_ok = response_str.contains("OK");
296 let has_error = response_str.contains("ENO");
297
298 Ok(has_apc && (has_ok || has_error))
299 }
300
301 #[cfg(not(unix))]
302 {
303 Ok(true)
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_window_size_calculation() {
314 let ws = WindowSize {
315 rows: 40,
316 cols: 120,
317 width: 1200,
318 height: 800,
319 };
320
321 assert_eq!(ws.cell_width(), 10);
322 assert_eq!(ws.cell_height(), 20);
323
324 let (cols, rows) = ws.cells_for_image(100, 100);
325 assert_eq!(cols, 10);
326 assert_eq!(rows, 5);
327 }
328
329 #[test]
330 fn test_window_size_edge_cases() {
331 let ws = WindowSize {
332 rows: 0,
333 cols: 0,
334 width: 0,
335 height: 0,
336 };
337
338 assert_eq!(ws.cell_width(), 0);
339 assert_eq!(ws.cell_height(), 0);
340 assert_eq!(ws.cells_for_image(100, 100), (0, 0));
341 }
342}