Skip to content

Commit

Permalink
feat: detect kitty through tmux (#406)
Browse files Browse the repository at this point in the history
This PR adds support for kitty protocol detection when you're using
tmux. This means now when you're running tmux inside kitty you no longer
need to explicitly configure the image protocol to be kitty-local/remote
and it will be instead figured out automatically.

This is done by making two kitty graphics protocol queries (and for
local and one for remote support) and another widely supported query
just in case the emulator doesn't support the kitty protocol and ignores
our queries (this is what's suggested
[here](https://sw.kovidgoyal.net/kitty/graphics-protocol/#querying-support-and-available-transmission-mediums)).
The [third query](https://vt100.net/docs/vt510-rm/DA1.html) being done
gets us the terminal capabilities; we figure out whether we support
sixel by parsing its output. This means now we always make these queries
once regardless of the protocol being used unless you explicitly
configure a protocol other than `auto` (the default).

The caveat here is that terminal emulators may not support [unicode
placeholders](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders)
which is a requirement for the kitty protocol to work while inside tmux.
At least wezterm [does not currently support
them](wez/wezterm#986) so images printed while
inside tmux in it look like crap. The workaround for now is to guess
that we're inside wezterm + tmux by checking an env var that wezterm
sets. This won't work if you start tmux in another terminal (since it
inherits _those_ env vars) but it's the best we can do. The outcome of
this is if there's another terminal that does support the kitty protocol
but does not have unicode placeholders implemented, images will look bad
and users will be forced to [specify the image
protocol](https://mfontanini.github.io/presenterm/guides/configuration.html#preferred-image-protocol)
to be used by hand.
  • Loading branch information
mfontanini authored Dec 20, 2024
2 parents ec192d1 + 7ea9cb1 commit 1809efb
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 117 deletions.
34 changes: 1 addition & 33 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ serde_with = "3.6"
strum = { version = "0.26", features = ["derive"] }
tempfile = "3.10"
tl = "0.7"
console = "0.15.8"
thiserror = "2"
unicode-width = "0.2"
os_pipe = "1.1.5"
Expand Down
6 changes: 3 additions & 3 deletions src/custom.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::{
GraphicsMode,
input::user::KeyBinding,
media::{emulator::TerminalEmulator, kitty::KittyMode},
media::{emulator::TerminalEmulator, kitty::KittyMode, query::TerminalCapabilities},
processing::code::SnippetLanguage,
};
use clap::ValueEnum;
Expand Down Expand Up @@ -270,10 +270,10 @@ impl TryFrom<&ImageProtocol> for GraphicsMode {
}
ImageProtocol::Iterm2 => GraphicsMode::Iterm2,
ImageProtocol::KittyLocal => {
GraphicsMode::Kitty { mode: KittyMode::Local, inside_tmux: TerminalEmulator::is_inside_tmux() }
GraphicsMode::Kitty { mode: KittyMode::Local, inside_tmux: TerminalCapabilities::is_inside_tmux() }
}
ImageProtocol::KittyRemote => {
GraphicsMode::Kitty { mode: KittyMode::Remote, inside_tmux: TerminalEmulator::is_inside_tmux() }
GraphicsMode::Kitty { mode: KittyMode::Remote, inside_tmux: TerminalCapabilities::is_inside_tmux() }
}
ImageProtocol::AsciiBlocks => GraphicsMode::AsciiBlocks,
#[cfg(feature = "sixel")]
Expand Down
60 changes: 27 additions & 33 deletions src/media/emulator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::kitty::local_mode_supported;
use super::query::TerminalCapabilities;
use crate::{GraphicsMode, media::kitty::KittyMode};
use std::env;
use strum::IntoEnumIterator;
Expand All @@ -19,10 +19,6 @@ pub enum TerminalEmulator {
}

impl TerminalEmulator {
pub fn is_inside_tmux() -> bool {
env::var("TERM_PROGRAM").ok().as_deref() == Some("tmux")
}

pub fn detect() -> Self {
let term = env::var("TERM").unwrap_or_default();
let term_program = env::var("TERM_PROGRAM").unwrap_or_default();
Expand All @@ -35,17 +31,17 @@ impl TerminalEmulator {
}

pub fn preferred_protocol(&self) -> GraphicsMode {
let inside_tmux = Self::is_inside_tmux();
let capabilities = TerminalCapabilities::query().unwrap_or_default();
let modes = [
GraphicsMode::Iterm2,
GraphicsMode::Kitty { mode: KittyMode::Local, inside_tmux },
GraphicsMode::Kitty { mode: KittyMode::Remote, inside_tmux },
GraphicsMode::Kitty { mode: KittyMode::Local, inside_tmux: capabilities.tmux },
GraphicsMode::Kitty { mode: KittyMode::Remote, inside_tmux: capabilities.tmux },
#[cfg(feature = "sixel")]
GraphicsMode::Sixel,
GraphicsMode::AsciiBlocks,
];
for mode in modes {
if self.supports_graphics_mode(&mode) {
if self.supports_graphics_mode(&mode, &capabilities) {
return mode;
}
}
Expand All @@ -68,41 +64,39 @@ impl TerminalEmulator {
}
}

fn supports_graphics_mode(&self, mode: &GraphicsMode) -> bool {
fn supports_graphics_mode(&self, mode: &GraphicsMode, capabilities: &TerminalCapabilities) -> bool {
match (mode, self) {
(GraphicsMode::Kitty { mode, inside_tmux }, Self::Kitty | Self::WezTerm) => match mode {
KittyMode::Local => local_mode_supported(*inside_tmux).unwrap_or_default(),
(GraphicsMode::Kitty { mode, .. }, Self::Kitty | Self::WezTerm) => match mode {
KittyMode::Local => capabilities.kitty_local,
KittyMode::Remote => true,
},
(GraphicsMode::Kitty { mode: KittyMode::Local, .. }, Self::Unknown) => {
// If we don't know the emulator but we detected that we support kitty use it,
// **unless** we are inside tmux and we "guess" that we're using wezterm. This is
// because wezterm's support for unicode placeholders (needed to display images in
// kitty when inside tmux) is not implemented (see
// https://github.com/wez/wezterm/issues/986).
//
// We can only really guess it's wezterm by checking environment variables and will
// not work if you started tmux on a different emulator and are running presenterm
// in wezterm.
capabilities.kitty_local && (!capabilities.tmux || !Self::guess_wezterm())
}
(GraphicsMode::Kitty { mode: KittyMode::Remote, .. }, Self::Unknown) => {
// Same as the above
capabilities.kitty_remote && (!capabilities.tmux || !Self::guess_wezterm())
}
(GraphicsMode::Iterm2, Self::Iterm2 | Self::WezTerm | Self::Mintty | Self::Konsole) => true,
(GraphicsMode::AsciiBlocks, _) => true,
#[cfg(feature = "sixel")]
(GraphicsMode::Sixel, Self::Foot | Self::Yaft | Self::Mlterm) => true,
#[cfg(feature = "sixel")]
(GraphicsMode::Sixel, Self::St | Self::Xterm) => supports_sixel().unwrap_or_default(),
(GraphicsMode::Sixel, Self::St | Self::Xterm | Self::Unknown) => capabilities.sixel,
_ => false,
}
}
}

#[cfg(feature = "sixel")]
fn supports_sixel() -> std::io::Result<bool> {
use console::{Key, Term};
use std::io::Write;

let mut term = Term::stdout();

write!(&mut term, "\x1b[c")?;
term.flush()?;

let mut response = String::new();
while let Ok(key) = term.read_key() {
if let Key::Char(c) = key {
response.push(c);
if c == 'c' {
break;
}
}
fn guess_wezterm() -> bool {
env::var("WEZTERM_EXECUTABLE").is_ok()
}
Ok(response.contains(";4;") || response.contains(";4c"))
}
56 changes: 9 additions & 47 deletions src/media/kitty.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use super::printer::{PrintImage, PrintImageError, PrintOptions, RegisterImageError, ResourceProperties};
use crate::style::Color;
use base64::{Engine, engine::general_purpose::STANDARD};
use console::{Key, Term};
use crossterm::{QueueableCommand, cursor::MoveToColumn, style::SetForegroundColor};
use image::{AnimationDecoder, Delay, DynamicImage, EncodableLayout, ImageReader, RgbaImage, codecs::gif::GifDecoder};
use rand::Rng;
Expand All @@ -12,7 +11,7 @@ use std::{
path::{Path, PathBuf},
sync::atomic::{AtomicU32, Ordering},
};
use tempfile::{NamedTempFile, TempDir, tempdir};
use tempfile::{TempDir, tempdir};

const IMAGE_PLACEHOLDER: &str = "\u{10EEEE}";
const DIACRITICS: &[u32] = &[
Expand Down Expand Up @@ -409,10 +408,10 @@ pub enum KittyMode {
Remote,
}

struct ControlCommand<'a, D> {
options: &'a [ControlOption],
payload: D,
tmux: bool,
pub(crate) struct ControlCommand<'a, D> {
pub(crate) options: &'a [ControlOption],
pub(crate) payload: D,
pub(crate) tmux: bool,
}

impl<D: fmt::Display> fmt::Display for ControlCommand<'_, D> {
Expand All @@ -438,7 +437,7 @@ impl<D: fmt::Display> fmt::Display for ControlCommand<'_, D> {
}

#[derive(Debug, Clone)]
enum ControlOption {
pub(crate) enum ControlOption {
Action(Action),
Format(ImageFormat),
Medium(TransmissionMedium),
Expand Down Expand Up @@ -483,7 +482,7 @@ impl fmt::Display for ControlOption {
}

#[derive(Debug, Clone)]
enum ImageFormat {
pub(crate) enum ImageFormat {
Rgba,
}

Expand All @@ -498,7 +497,7 @@ impl fmt::Display for ImageFormat {
}

#[derive(Debug, Clone)]
enum TransmissionMedium {
pub(crate) enum TransmissionMedium {
Direct,
LocalFile,
}
Expand All @@ -515,7 +514,7 @@ impl fmt::Display for TransmissionMedium {
}

#[derive(Debug, Clone)]
enum Action {
pub(crate) enum Action {
Animate,
TransmitAndDisplay,
TransmitFrame,
Expand All @@ -534,40 +533,3 @@ impl fmt::Display for Action {
write!(f, "{value}")
}
}

pub(crate) fn local_mode_supported(inside_tmux: bool) -> io::Result<bool> {
let mut file = NamedTempFile::new()?;
let image = DynamicImage::new_rgba8(1, 1);
file.write_all(image.into_rgba8().as_raw().as_bytes())?;
file.flush()?;
let Some(path) = file.path().as_os_str().to_str() else {
return Ok(false);
};
let encoded_path = STANDARD.encode(path);

let options = &[
ControlOption::Format(ImageFormat::Rgba),
ControlOption::Action(Action::Query),
ControlOption::Medium(TransmissionMedium::LocalFile),
ControlOption::ImageId(rand::random()),
ControlOption::Width(1),
ControlOption::Height(1),
];
let mut writer = io::stdout();
let command = ControlCommand { options, payload: encoded_path, tmux: inside_tmux };
write!(writer, "{command}")?;
writer.flush()?;

let term = Term::stdout();
let mut response = String::new();
while let Ok(key) = term.read_key() {
match key {
Key::Unknown => break,
Key::UnknownEscSeq(seq) if seq == ['\\'] => break,
Key::Char(c) => response.push(c),
_ => continue,
}
}

Ok(response.ends_with(";OK"))
}
1 change: 1 addition & 0 deletions src/media/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub(crate) mod image;
mod iterm;
pub(crate) mod kitty;
pub(crate) mod printer;
pub(crate) mod query;
pub(crate) mod register;
pub(crate) mod scale;
#[cfg(feature = "sixel")]
Expand Down
Loading

0 comments on commit 1809efb

Please sign in to comment.