Skip to content

Commit 3ddb486

Browse files
authored
Support tmux for the kitty protocol (#155)
* Support tmux * Update documents
1 parent 49087b4 commit 3ddb486

4 files changed

Lines changed: 80 additions & 26 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,18 @@ These image protocols are supported:
116116

117117
- [Inline Images Protocol (iTerm2)](https://iterm2.com/documentation-images.html)
118118
- [Terminal graphics protocol (kitty)](https://sw.kovidgoyal.net/kitty/graphics-protocol/)
119-
- Supports both the existing graphics protocol mode and [the Unicode placeholder](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) mode.
119+
- Supports both the existing graphics protocol mode and the [Unicode placeholder](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders) mode.
120120

121121
For more information, see [Compatibility](https://lusingander.github.io/serie/getting-started/compatibility.html).
122122

123+
### Partially supported environments
124+
125+
- tmux is supported only when using the kitty Unicode placeholder protocol.
126+
123127
### Unsupported environments
124128

125129
- Sixel graphics is not supported.
126-
- Terminal multiplexers (screen, tmux, Zellij, etc.) are not supported.
130+
- Other terminal multiplexers (screen, Zellij, etc.) other than those listed in [Partially supported environments](#partially-supported-environments) are not supported.
127131
- Windows is not officially supported. Please refer to [the related issue](https://github.com/lusingander/serie/issues/147#issuecomment-4192875627).
128132

129133
## Screenshots

docs/src/getting-started/compatibility.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,13 @@ The terminals on which each has been confirmed to work are listed below.
3030

3131
Rendering using Unicode Placeholder is available by explicitly specifying `kitty-unicode` as `protocol` option or config.
3232

33-
## Unsupported environments
33+
### Partially supported environments
34+
35+
- tmux is supported only when using the kitty Unicode placeholder protocol.
36+
- Requires `set -g allow-passthrough on` in tmux.conf (version 3.2+).
37+
38+
### Unsupported environments
3439

3540
- Sixel graphics is not supported.
36-
- Terminal multiplexers (screen, tmux, Zellij, etc.) are not supported.
41+
- Other terminal multiplexers (screen, Zellij, etc.) other than those listed in [Partially supported environments](#partially-supported-environments) are not supported.
3742
- Windows is not officially supported. Please refer to [the related issue](https://github.com/lusingander/serie/issues/147#issuecomment-4192875627).

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ impl From<Option<ImageProtocolType>> for protocol::ImageProtocol {
6363
Some(ImageProtocolType::Auto) => protocol::auto_detect(),
6464
Some(ImageProtocolType::Iterm) => protocol::ImageProtocol::Iterm2,
6565
Some(ImageProtocolType::Kitty) => protocol::ImageProtocol::Kitty,
66-
Some(ImageProtocolType::KittyUnicode) => protocol::ImageProtocol::KittyUnicode,
66+
Some(ImageProtocolType::KittyUnicode) => protocol::ImageProtocol::KittyUnicode {
67+
tmux: protocol::detect_tmux(),
68+
},
6769
None => protocol::auto_detect(),
6870
}
6971
}

src/protocol.rs

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,38 @@ use ratatui::style::{Color, Style};
99
// By default assume the Iterm2 is the best protocol to use for all terminals *unless* an env
1010
// variable is set that suggests the terminal is probably Kitty.
1111
pub fn auto_detect() -> ImageProtocol {
12-
// https://sw.kovidgoyal.net/kitty/glossary/#envvar-KITTY_WINDOW_ID
13-
if env::var("KITTY_WINDOW_ID").is_ok() {
14-
return ImageProtocol::Kitty;
12+
if detect_kitty_graphics_protocol() {
13+
if detect_tmux() {
14+
ImageProtocol::KittyUnicode { tmux: true }
15+
} else {
16+
ImageProtocol::Kitty
17+
}
18+
} else {
19+
ImageProtocol::Iterm2
1520
}
21+
}
22+
23+
fn detect_kitty_graphics_protocol() -> bool {
24+
// kitty
25+
// https://sw.kovidgoyal.net/kitty/glossary/#envvar-KITTY_WINDOW_ID
26+
env::var("KITTY_WINDOW_ID").is_ok()
27+
// ghostty
1628
// https://ghostty.org/docs/help/terminfo
17-
if env::var("TERM").is_ok_and(|t| t == "xterm-ghostty")
18-
|| env::var("GHOSTTY_RESOURCES_DIR").is_ok()
19-
{
20-
return ImageProtocol::Kitty;
21-
}
22-
ImageProtocol::Iterm2
29+
|| env::var("TERM").ok().is_some_and(|t| t == "xterm-ghostty")
30+
|| env::var("GHOSTTY_RESOURCES_DIR").is_ok()
31+
}
32+
33+
pub fn detect_tmux() -> bool {
34+
env::var("TMUX").is_ok_and(|tmux| !tmux.is_empty())
35+
|| env::var("TERM").is_ok_and(|term| term.starts_with("tmux"))
36+
|| env::var("TERM_PROGRAM").is_ok_and(|term_program| term_program == "tmux")
2337
}
2438

2539
#[derive(Debug, Clone, Copy)]
2640
pub enum ImageProtocol {
2741
Iterm2,
2842
Kitty,
29-
KittyUnicode,
43+
KittyUnicode { tmux: bool },
3044
}
3145

3246
#[derive(Debug, Clone)]
@@ -76,8 +90,8 @@ impl ImageProtocol {
7690
let symbol = match self {
7791
ImageProtocol::Iterm2 => iterm2_encode(bytes, cell_width, 1),
7892
ImageProtocol::Kitty => kitty_encode(bytes, cell_width, 1),
79-
ImageProtocol::KittyUnicode => {
80-
return kitty_unicode_prepare(bytes, cell_width, image_id);
93+
ImageProtocol::KittyUnicode { tmux } => {
94+
return kitty_unicode_prepare(bytes, cell_width, image_id, *tmux);
8195
}
8296
};
8397
let mut cells = Vec::with_capacity(cell_width);
@@ -104,22 +118,22 @@ impl ImageProtocol {
104118
match self {
105119
ImageProtocol::Iterm2 => {}
106120
ImageProtocol::Kitty => kitty_clear_line(y),
107-
ImageProtocol::KittyUnicode => {}
121+
ImageProtocol::KittyUnicode { .. } => {}
108122
}
109123
}
110124

111125
pub fn clear(&self) {
112126
match self {
113127
ImageProtocol::Iterm2 => {}
114128
ImageProtocol::Kitty => kitty_clear(),
115-
ImageProtocol::KittyUnicode => {}
129+
ImageProtocol::KittyUnicode { .. } => {}
116130
}
117131
}
118132

119133
pub fn delete_images(&self, image_ids: &[u32]) -> Result<(), std::io::Error> {
120134
match self {
121135
ImageProtocol::Iterm2 | ImageProtocol::Kitty => Ok(()),
122-
ImageProtocol::KittyUnicode => kitty_unicode_delete_images(image_ids),
136+
ImageProtocol::KittyUnicode { tmux } => kitty_unicode_delete_images(image_ids, *tmux),
123137
}
124138
}
125139
}
@@ -468,9 +482,14 @@ fn kitty_encode(bytes: &[u8], cell_width: usize, cell_height: usize) -> String {
468482
s
469483
}
470484

471-
fn kitty_unicode_prepare(bytes: &[u8], cell_width: usize, image_id: u32) -> PreparedImage {
485+
fn kitty_unicode_prepare(
486+
bytes: &[u8],
487+
cell_width: usize,
488+
image_id: u32,
489+
tmux: bool,
490+
) -> PreparedImage {
472491
let mut cells = Vec::with_capacity(cell_width);
473-
let upload_symbol = kitty_unicode_encode(bytes, cell_width, 1, image_id);
492+
let upload_symbol = kitty_unicode_encode(bytes, cell_width, 1, image_id, tmux);
474493
let foreground = Color::Rgb(
475494
((image_id >> 16) & 0xff) as u8,
476495
((image_id >> 8) & 0xff) as u8,
@@ -507,6 +526,7 @@ fn kitty_unicode_encode(
507526
cell_width: usize,
508527
cell_height: usize,
509528
image_id: u32,
529+
tmux: bool,
510530
) -> String {
511531
let base64_str = to_base64_str(bytes);
512532
let chunk_size = 4096;
@@ -516,8 +536,12 @@ fn kitty_unicode_encode(
516536
let chunks = base64_str.as_bytes().chunks(chunk_size);
517537
let total_chunks = chunks.len();
518538

539+
let (start, esc, end) = passthrough_escapes(tmux);
540+
519541
for (i, chunk) in chunks.enumerate() {
520-
s.push_str("\x1b_G");
542+
s.push_str(start);
543+
s.push_str(esc);
544+
s.push_str("_G");
521545
if i == 0 {
522546
s.push_str(&format!(
523547
"a=T,f=100,U=1,q=2,i={image_id},c={cell_width},r={cell_height},"
@@ -529,7 +553,9 @@ fn kitty_unicode_encode(
529553
s.push_str("m=0;");
530554
}
531555
s.push_str(std::str::from_utf8(chunk).unwrap());
532-
s.push_str("\x1b\\");
556+
s.push_str(esc);
557+
s.push('\\');
558+
s.push_str(end);
533559
}
534560

535561
s
@@ -548,14 +574,31 @@ fn kitty_clear() {
548574
print!("\x1b_Ga=d,d=A;\x1b\\");
549575
}
550576

551-
fn kitty_unicode_delete_images(image_ids: &[u32]) -> Result<(), io::Error> {
577+
fn kitty_unicode_delete_images(image_ids: &[u32], tmux: bool) -> Result<(), io::Error> {
552578
if image_ids.is_empty() {
553579
return Ok(());
554580
}
555581

556582
let mut stdout = io::stdout().lock();
557583
for image_id in image_ids {
558-
write!(stdout, "\x1b_Ga=d,d=I,i={image_id}\x1b\\")?;
584+
write!(
585+
stdout,
586+
"{}",
587+
kitty_unicode_delete_image_encode(*image_id, tmux)
588+
)?;
559589
}
560590
stdout.flush()
561591
}
592+
593+
fn kitty_unicode_delete_image_encode(image_id: u32, tmux: bool) -> String {
594+
let (start, esc, end) = passthrough_escapes(tmux);
595+
format!("{start}{esc}_Ga=d,d=I,i={image_id}{esc}\\{end}")
596+
}
597+
598+
fn passthrough_escapes(tmux: bool) -> (&'static str, &'static str, &'static str) {
599+
if tmux {
600+
("\x1bPtmux;", "\x1b\x1b", "\x1b\\")
601+
} else {
602+
("", "\x1b", "")
603+
}
604+
}

0 commit comments

Comments
 (0)