Skip to content

Commit

Permalink
feat: detect kitty through tmux
Browse files Browse the repository at this point in the history
  • Loading branch information
mfontanini committed Dec 20, 2024
1 parent ec192d1 commit 7ea9cb1
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 7ea9cb1

Please sign in to comment.