Skip to content

Commit eac5eb5

Browse files
committed
fix(iterm): add fallback check for "ReportCellSize"
As a indicator if iterm image protocol is supported.
1 parent 64ed443 commit eac5eb5

File tree

2 files changed

+160
-9
lines changed

2 files changed

+160
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
- Remove `lazy_static` dependency in favor of `std::sync::LazyLock`
44
- MSRV is now 1.80
55
- Use sixel if found in device attributes instead of static TERM list
6-
- Detect iterm2 protocol support by querying capabilities
6+
- Detect iterm2 protocol support by querying `1337;Capabilities` or `1337;ReportCellSize`
77

88
## 0.9.2
99
- Use iterm and sixel in more terminals

src/printer/iterm.rs

Lines changed: 159 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ impl Printer for iTermPrinter {
3232
img: &DynamicImage,
3333
config: &Config,
3434
) -> ViuResult<(u32, u32)> {
35+
// DEBUG: REMOVE THIS BEFORE MERGE
36+
eprintln!("using iterm");
3537
let (width, height) = img.dimensions();
3638

3739
// Transform the dynamic image to a PNG which can be given directly to iTerm
@@ -96,7 +98,7 @@ const ITERM_CAP_REPLY_SIZE: usize = "1337;Capabilities=".len();
9698
/// Check if the terminal supports "iterm2 inline image protocol" by querying capabilities.
9799
///
98100
/// This function is based on what is written in <https://iterm2.com/feature-reporting/> and <https://gitlab.com/gnachman/iterm2/-/issues/10236>.
99-
fn has_iterm_support(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult {
101+
fn has_iterm_support_capabilities(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult {
100102
// send the query
101103
write!(
102104
stdout,
@@ -124,10 +126,10 @@ fn has_iterm_support(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult
124126
}
125127

126128
// DEBUG: REMOVE THIS BEFORE MERGE
127-
eprintln!("Iterm2 Response: {:#?}", response);
129+
eprintln!("Iterm2 Response Capabilities: {:#?}", response);
128130

129131
// no response to the "Capabilities" query, or pre-maturely ended without the DSR
130-
if response.last() != Some(&end_seq) || response.len() < ITERM_CAP_REPLY_SIZE + 1/* BEL */ + 1
132+
if response.last() != Some(&end_seq) || response.len() < ITERM_CAP_REPLY_SIZE + 1/* ST */ + 1
131133
/* END SEQ */
132134
{
133135
return Err(ViuError::ItermResponse(response));
@@ -148,11 +150,85 @@ fn has_iterm_support(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult
148150
Err(ViuError::ItermResponse(response))
149151
}
150152

153+
const ITERM_CELL_REPLY_SIZE: usize = "1337;ReportCellSize=".len();
154+
155+
/// Check if the terminal liekly supports "iterm2 inline image protocol" by querying ReportCellSize.
156+
///
157+
/// This function is based on what is written in <https://iterm2.com/documentation-escape-codes.html#report-cell-size> and <https://github.com/atanunq/viuer/pull/88>.
158+
fn has_iterm_support_reportcellsize(stdout: &mut impl Write, stdin: &impl ReadKey) -> ViuResult {
159+
// send the query
160+
write!(
161+
stdout,
162+
// Query iterm2 ReportCellSize https://iterm2.com/documentation-escape-codes.html#report-cell-size
163+
"\x1b]1337;ReportCellSize\x1b\\"
164+
)?;
165+
// send extra "Device Status Report (DSR)" which practically all terminals respond to, to avoid infinitely blocking if not replied to the query above
166+
// see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_
167+
write!(stdout, "\x1b[5n")?;
168+
169+
stdout.flush()?;
170+
171+
let mut response = Vec::new();
172+
173+
let end_seq = Key::UnknownEscSeq(vec!['[', '0', 'n']);
174+
175+
while let Ok(key) = stdin.read_key() {
176+
// The response will end with the reply to "\x1b[5n", which is "\x1b[0n"
177+
// Also, break if the Unknown key is found, which is returned when we're not in a tty
178+
let should_break = key == end_seq || key == Key::Unknown;
179+
response.push(key);
180+
if should_break {
181+
break;
182+
}
183+
}
184+
185+
// DEBUG: REMOVE THIS BEFORE MERGE
186+
eprintln!("Iterm2 Response ReportCellSize: {:#?}", response);
187+
188+
// no response to the "Capabilities" query, or pre-maturely ended without the DSR
189+
if response.last() != Some(&end_seq) || response.len() < ITERM_CELL_REPLY_SIZE + 1/* ST */ + 1
190+
/* END SEQ */
191+
{
192+
return Err(ViuError::ItermResponse(response));
193+
}
194+
195+
// Check if the response we got actually contains the correct response (we only check the echo name)
196+
const EXPECTED_RESPONSE: &str = "ReportCellSize";
197+
198+
let mut chars = EXPECTED_RESPONSE.chars().peekable();
199+
let mut consumed: usize = 0;
200+
201+
// Check each key in the response and try to exhaust the "chars" iter, which will indicate the full response is available
202+
for key in &response {
203+
if chars.peek().map(|v| Key::Char(*v)).as_ref() == Some(key) {
204+
let _ = chars.next();
205+
consumed += 1;
206+
} else if consumed != 0 {
207+
// we hit something not in the correct sequence, so we break here
208+
// for example we hit "ReportVariable" where we want to stop at "V" instead of trying to continue
209+
break;
210+
}
211+
}
212+
213+
if chars.next().is_none() {
214+
return Ok(());
215+
}
216+
217+
Err(ViuError::ItermResponse(response))
218+
}
219+
151220
/// Check if the iTerm protocol can be used
152221
fn check_iterm_support() -> bool {
153222
let mut stdout = std::io::stdout();
154223
let term = Term::stdout();
155-
if has_iterm_support(&mut stdout, &term).is_ok() {
224+
if has_iterm_support_capabilities(&mut stdout, &term).is_ok() {
225+
// DEBUG: REMOVE THIS BEFORE MERGE
226+
eprintln!("Capabilities OK");
227+
return true;
228+
}
229+
if has_iterm_support_reportcellsize(&mut stdout, &term).is_ok() {
230+
// DEBUG: REMOVE THIS BEFORE MERGE
231+
eprintln!("ReportCellSize OK");
156232
return true;
157233
}
158234

@@ -212,7 +288,8 @@ mod tests {
212288

213289
let test_data = [
214290
// intro
215-
Key::UnknownEscSeq([']', '1'].into()), // TODO: is this actually returning that, or differently? i dont know how "console" handles OSC
291+
Key::UnknownEscSeq(vec![']']), // TODO: is this actually returning that, or differently? i dont know how "console" handles OSC
292+
Key::Char('1'),
216293
Key::Char('3'),
217294
Key::Char('3'),
218295
Key::Char('7'),
@@ -239,14 +316,17 @@ mod tests {
239316
Key::Char('F'),
240317
Key::Char('S'),
241318
Key::Char('x'),
242-
// BEL / Bell
319+
// ST
243320
Key::UnknownEscSeq(vec!['\\']),
244321
// DSR
245322
Key::UnknownEscSeq(vec!['[', '0', 'n']),
246323
];
247324
let test_response = TestKeys::new(&test_data);
248325

249-
assert_eq!(has_iterm_support(&mut vec, &test_response).unwrap(), ());
326+
assert_eq!(
327+
has_iterm_support_capabilities(&mut vec, &test_response).unwrap(),
328+
()
329+
);
250330
let stdout = std::str::from_utf8(&vec).unwrap();
251331

252332
assert_eq!(stdout, "\x1b]1337;Capabilities\x1b\\\x1b[5n");
@@ -263,10 +343,81 @@ mod tests {
263343
];
264344
let test_response = TestKeys::new(&test_data);
265345

266-
assert!(has_iterm_support(&mut vec, &test_response).is_err());
346+
assert!(has_iterm_support_capabilities(&mut vec, &test_response).is_err());
267347
let stdout = std::str::from_utf8(&vec).unwrap();
268348

269349
assert_eq!(stdout, "\x1b]1337;Capabilities\x1b\\\x1b[5n");
270350
assert!(test_response.reached_end());
271351
}
352+
353+
#[test]
354+
fn reportcellsize_iterm2_should_reply() {
355+
// output captured on konsole 25.08.1
356+
let mut vec = Vec::new();
357+
358+
let test_data = [
359+
// intro
360+
Key::UnknownEscSeq(vec![']']),
361+
Key::Char('1'),
362+
Key::Char('3'),
363+
Key::Char('3'),
364+
Key::Char('7'),
365+
Key::Char(';'),
366+
Key::Char('R'),
367+
Key::Char('e'),
368+
Key::Char('p'),
369+
Key::Char('o'),
370+
Key::Char('r'),
371+
Key::Char('t'),
372+
Key::Char('C'),
373+
Key::Char('e'),
374+
Key::Char('l'),
375+
Key::Char('l'),
376+
Key::Char('S'),
377+
Key::Char('i'),
378+
Key::Char('z'),
379+
Key::Char('e'),
380+
Key::Char('='),
381+
// actual response
382+
Key::Char('1'),
383+
Key::Char('5'),
384+
Key::Char('.'),
385+
Key::Char('0'),
386+
Key::Char(';'),
387+
Key::Char('1'),
388+
Key::Char('.'),
389+
Key::Char('0'),
390+
// BEL / Bell
391+
Key::Char('\x07'),
392+
// DSR
393+
Key::UnknownEscSeq(vec!['[', '0', 'n']),
394+
];
395+
let test_response = TestKeys::new(&test_data);
396+
397+
assert_eq!(
398+
has_iterm_support_reportcellsize(&mut vec, &test_response).unwrap(),
399+
()
400+
);
401+
let stdout = std::str::from_utf8(&vec).unwrap();
402+
403+
assert_eq!(stdout, "\x1b]1337;ReportCellSize\x1b\\\x1b[5n");
404+
assert!(test_response.reached_end());
405+
}
406+
407+
#[test]
408+
fn reportcellsize_should_handle_no_reply() {
409+
let mut vec = Vec::new();
410+
411+
let test_data = [
412+
// DSR
413+
Key::UnknownEscSeq(['[', '0', 'n'].into()),
414+
];
415+
let test_response = TestKeys::new(&test_data);
416+
417+
assert!(has_iterm_support_reportcellsize(&mut vec, &test_response).is_err());
418+
let stdout = std::str::from_utf8(&vec).unwrap();
419+
420+
assert_eq!(stdout, "\x1b]1337;ReportCellSize\x1b\\\x1b[5n");
421+
assert!(test_response.reached_end());
422+
}
272423
}

0 commit comments

Comments
 (0)