mod geometry;
mod iterm;
mod kitty;
mod konsole;
mod protocol;
mod sixel;
mod window;
use anyhow::{Context, Result};
use ratatui::layout::Rect;
use std::{env, io::Write as _, path::Path};
use crate::app::App;
pub(in crate::app) use self::geometry::{
area_pixel_size, fit_image_area, fit_image_pixels, read_png_dimensions,
};
pub(in crate::app) use self::iterm::encode_iterm_inline_payload;
pub(in crate::app) use self::protocol::{command_exists, select_image_protocol};
use self::protocol::{detect_terminal_identity, pdf_preview_tools_available};
pub(in crate::app) use self::sixel::{encode_sixel_dcs, place_sixel_from_dcs};
use self::window::query_terminal_window_size;
pub(in crate::app) fn preview_log(msg: impl std::fmt::Display) {
if env::var_os("ELIO_DEBUG_PREVIEW").is_none() {
return;
}
let _ = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(std::env::temp_dir().join("elio-preview.log"))
.and_then(|mut f| writeln!(f, "{msg}"));
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(in crate::app) struct TerminalImageState {
pub(super) protocol: ImageProtocol,
pub(super) identity: TerminalIdentity,
pub(super) window: Option<TerminalWindowSize>,
pending_iterm_erase: Vec<Rect>,
pending_resize_clear: bool,
pending_iterm_popup_restore: bool,
pending_sixel_repaint: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::app) enum OverlayPresentState {
NotRequested,
Waiting,
Displayed,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(in crate::app) enum TerminalIdentity {
Kitty,
Ghostty,
Warp,
WezTerm,
ITerm2,
Konsole,
Alacritty,
Foot,
WindowsTerminal,
#[default]
Other,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(in crate::app) enum ImageProtocol {
KittyGraphics,
KonsoleGraphics,
ItermInline,
Sixel,
#[default]
None,
}
impl ImageProtocol {
pub(in crate::app) fn is_raster(self) -> bool {
matches!(self, ImageProtocol::ItermInline | ImageProtocol::Sixel)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(in crate::app) struct TerminalWindowSize {
pub(super) cells_width: u16,
pub(super) cells_height: u16,
pub(super) pixels_width: u32,
pub(super) pixels_height: u32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::app) struct RenderedImageDimensions {
pub(super) width_px: u32,
pub(super) height_px: u32,
}
impl App {
pub(crate) fn enable_terminal_image_previews(&mut self) {
let identity = detect_terminal_identity();
let image_previews_override = env::var_os("ELIO_IMAGE_PREVIEWS").is_some();
let protocol = select_image_protocol(identity, image_previews_override);
preview_log(format_args!(
"enable_terminal_image_previews:\n TERM={}\n TERM_PROGRAM={}\n KITTY_WINDOW_ID={}\n WARP_SESSION_ID={}\n WT_SESSION={}\n KONSOLE_DBUS_SESSION={}\n KONSOLE_DBUS_SERVICE={}\n KONSOLE_DBUS_WINDOW={}\n identity={identity:?}\n override={image_previews_override}\n protocol={protocol:?}",
env::var("TERM").unwrap_or_default(),
env::var("TERM_PROGRAM").unwrap_or_default(),
env::var_os("KITTY_WINDOW_ID").is_some(),
env::var_os("WARP_SESSION_ID").is_some(),
env::var_os("WT_SESSION").is_some(),
env::var_os("KONSOLE_DBUS_SESSION").is_some(),
env::var_os("KONSOLE_DBUS_SERVICE").is_some(),
env::var_os("KONSOLE_DBUS_WINDOW").is_some(),
));
self.preview.terminal_images.identity = identity;
self.preview.terminal_images.protocol = protocol;
self.preview.pdf.pdf_tools_available = pdf_preview_tools_available();
self.refresh_terminal_image_window_size();
preview_log(format_args!(
" window={:?}",
self.preview.terminal_images.window
));
self.sync_pdf_preview_selection();
}
pub(crate) fn handle_terminal_image_resize(&mut self) {
self.refresh_terminal_image_window_size();
if matches!(
self.preview.terminal_images.protocol,
ImageProtocol::KittyGraphics | ImageProtocol::Sixel
) && (self.static_image_overlay_displayed() || self.pdf_overlay_displayed())
{
self.preview.terminal_images.pending_resize_clear = true;
}
self.handle_pdf_overlay_resize();
}
pub(crate) fn take_pending_resize_clear(&mut self) -> bool {
if !self.preview.terminal_images.pending_resize_clear {
return false;
}
self.preview.terminal_images.pending_resize_clear = false;
self.clear_displayed_static_image();
self.clear_displayed_pdf_overlay();
true
}
pub(in crate::app) fn terminal_image_overlay_available(&self) -> bool {
self.preview.terminal_images.protocol != ImageProtocol::None
}
pub(in crate::app) fn uses_sixel_image_protocol(&self) -> bool {
self.preview.terminal_images.protocol == ImageProtocol::Sixel
}
pub(crate) fn is_windows_terminal(&self) -> bool {
self.preview.terminal_images.identity == TerminalIdentity::WindowsTerminal
}
pub(in crate::app) fn needs_sixel_repaint_workaround(&self) -> bool {
self.preview.terminal_images.protocol == ImageProtocol::Sixel
&& self.preview.terminal_images.identity == TerminalIdentity::Foot
}
pub(in crate::app) fn needs_slow_sixel_navigation_workaround(&self) -> bool {
self.preview.terminal_images.protocol == ImageProtocol::Sixel
&& matches!(
self.preview.terminal_images.identity,
TerminalIdentity::Foot | TerminalIdentity::WindowsTerminal
)
}
#[cfg(test)]
pub(in crate::app) fn set_terminal_image_protocol_for_tests(
&mut self,
protocol: ImageProtocol,
identity: TerminalIdentity,
) {
self.preview.terminal_images.protocol = protocol;
self.preview.terminal_images.identity = identity;
}
pub(in crate::app) fn cached_terminal_window(&self) -> Option<TerminalWindowSize> {
self.preview.terminal_images.window
}
pub(crate) fn kitty_pre_draw_erase(&self) -> Vec<u8> {
if self.preview.terminal_images.protocol != ImageProtocol::KittyGraphics {
return Vec::new();
}
let keep_stale = self.keep_displayed_static_image_overlay_while_pending();
let needs_clear = (self.static_image_overlay_displayed()
&& !self.displayed_static_image_matches_active()
&& !keep_stale)
|| (self.pdf_overlay_displayed() && !self.displayed_pdf_overlay_matches_active());
if !needs_clear {
return Vec::new();
}
self.displayed_static_image_clear_area()
.or_else(|| self.displayed_pdf_overlay_area())
.map(iterm::erase_cells)
.unwrap_or_default()
}
pub(crate) fn iterm_pre_draw_erase(&mut self) -> Vec<u8> {
if !self.preview.terminal_images.protocol.is_raster() {
return Vec::new();
}
let mut areas = std::mem::take(&mut self.preview.terminal_images.pending_iterm_erase);
let keep_stale = self.keep_displayed_static_image_overlay_while_pending();
if self.static_image_overlay_displayed()
&& !self.displayed_static_image_matches_active()
&& !keep_stale
&& let Some(area) = self.displayed_static_image_clear_area()
{
geometry::push_unique_rect(&mut areas, area);
}
if self.pdf_overlay_displayed()
&& !self.displayed_pdf_overlay_matches_active()
&& let Some(area) = self.displayed_pdf_overlay_area()
{
geometry::push_unique_rect(&mut areas, area);
}
if areas.is_empty() {
return Vec::new();
}
let mut expanded_areas = Vec::with_capacity(areas.len());
for area in areas {
geometry::push_unique_rect(
&mut expanded_areas,
if self.preview.terminal_images.identity == TerminalIdentity::WindowsTerminal
&& self.preview.terminal_images.protocol == ImageProtocol::Sixel
{
iterm::expand_raster_erase_area(&self.input.frame_state, area, 1, 1)
} else {
iterm::expand_raster_erase_area(&self.input.frame_state, area, 0, 2)
},
);
}
expanded_areas
.into_iter()
.flat_map(iterm::erase_cells)
.collect()
}
pub(crate) fn present_preview_overlay(&mut self) -> Result<Vec<u8>> {
if self.browser_wheel_burst_active() || self.preview.state.deferred_refresh_at.is_some() {
return Ok(Vec::new());
}
let protocol = self.preview.terminal_images.protocol;
if protocol == ImageProtocol::None {
preview_log("present_preview_overlay: no protocol -> clear");
return self.clear_preview_overlay();
}
let popup_open = self.any_modal_overlay_open();
if protocol == ImageProtocol::KonsoleGraphics && popup_open {
if self.static_image_overlay_displayed() || self.pdf_overlay_displayed() {
return self.clear_preview_overlay();
}
return Ok(Vec::new());
}
if protocol.is_raster()
&& popup_open
&& (self.static_image_overlay_displayed() || self.pdf_overlay_displayed())
{
self.preview.terminal_images.pending_iterm_popup_restore = true;
}
let force_sixel_repaint = protocol == ImageProtocol::Sixel
&& std::mem::take(&mut self.preview.terminal_images.pending_sixel_repaint);
let force_iterm_popup_repaint = protocol.is_raster()
&& self.preview.terminal_images.pending_iterm_popup_restore
&& !popup_open;
let force_protocol_repaint = force_iterm_popup_repaint || force_sixel_repaint;
let excluded: Vec<Rect> = if protocol == ImageProtocol::KittyGraphics {
self.collect_popup_rects()
} else {
Vec::new()
};
let keep_stale_page_preview_overlay =
self.keep_displayed_static_image_overlay_while_pending();
let mut out = Vec::new();
if (self.static_image_overlay_displayed()
&& !self.displayed_static_image_matches_active()
&& !keep_stale_page_preview_overlay)
|| self.pdf_overlay_displayed() && !self.displayed_pdf_overlay_matches_active()
{
out.extend(self.clear_preview_overlay()?);
}
let static_state = self.present_static_image_overlay(
protocol,
&excluded,
force_protocol_repaint,
&mut out,
)?;
preview_log(format_args!(
"present_preview_overlay: protocol={protocol:?} static={static_state:?} out_len={}",
out.len()
));
match static_state {
OverlayPresentState::Displayed | OverlayPresentState::Waiting => return Ok(out),
OverlayPresentState::NotRequested => {}
}
let pdf_state =
self.present_pdf_overlay(protocol, &excluded, force_protocol_repaint, &mut out)?;
preview_log(format_args!(
"present_preview_overlay: pdf={pdf_state:?} out_len={}",
out.len()
));
match pdf_state {
OverlayPresentState::Displayed | OverlayPresentState::Waiting => return Ok(out),
OverlayPresentState::NotRequested => {}
}
let visual_state = self.present_preview_visual_overlay(
protocol,
&excluded,
force_protocol_repaint,
&mut out,
)?;
preview_log(format_args!(
"present_preview_overlay: visual={visual_state:?} out_len={}",
out.len()
));
match visual_state {
OverlayPresentState::Displayed | OverlayPresentState::Waiting => Ok(out),
OverlayPresentState::NotRequested if keep_stale_page_preview_overlay => Ok(out),
OverlayPresentState::NotRequested => {
self.preview.terminal_images.pending_iterm_popup_restore = false;
out.extend(self.clear_preview_overlay()?);
Ok(out)
}
}
}
fn collect_popup_rects(&self) -> Vec<Rect> {
let mut rects = Vec::new();
if let Some(r) = self.input.frame_state.trash_panel {
rects.push(r);
}
if let Some(r) = self.input.frame_state.restore_panel {
rects.push(r);
}
if let Some(r) = self.input.frame_state.create_panel {
rects.push(r);
}
if let Some(r) = self.input.frame_state.rename_panel {
rects.push(r);
}
if let Some(r) = self.input.frame_state.goto_panel {
rects.push(r);
}
if let Some(r) = self.input.frame_state.copy_panel {
rects.push(r);
}
if let Some(r) = self.input.frame_state.open_with_panel {
rects.push(r);
}
if let Some(r) = self.input.frame_state.search_panel {
rects.push(r);
}
if let Some(r) = self.input.frame_state.help_panel {
rects.push(r);
}
rects
}
fn any_modal_overlay_open(&self) -> bool {
self.overlays.trash.is_some()
|| self.overlays.restore.is_some()
|| self.overlays.create.is_some()
|| self.overlays.rename.is_some()
|| self.overlays.bulk_rename.is_some()
|| self.overlays.goto.is_some()
|| self.overlays.copy.is_some()
|| self.overlays.open_with.is_some()
|| self.overlays.search.is_some()
|| self.overlays.help
}
pub(in crate::app) fn clear_pending_iterm_popup_restore(&mut self) {
self.preview.terminal_images.pending_iterm_popup_restore = false;
}
pub(in crate::app) fn queue_sixel_repaint(&mut self) {
if self.needs_sixel_repaint_workaround() {
self.preview.terminal_images.pending_sixel_repaint = true;
}
}
pub(in crate::app) fn queue_windows_terminal_pdf_sixel_repaint(&mut self) {
if self.preview.terminal_images.protocol == ImageProtocol::Sixel
&& self.preview.terminal_images.identity == TerminalIdentity::WindowsTerminal
{
self.preview.terminal_images.pending_sixel_repaint = true;
}
}
pub(crate) fn clear_preview_overlay(&mut self) -> Result<Vec<u8>> {
if !self.static_image_overlay_displayed() && !self.pdf_overlay_displayed() {
return Ok(Vec::new());
}
let bytes = clear_terminal_images(self.preview.terminal_images.protocol)
.context("failed to clear preview overlay")?;
self.clear_pending_iterm_popup_restore();
self.clear_displayed_static_image();
self.clear_displayed_pdf_overlay();
Ok(bytes)
}
pub(crate) fn queue_forced_iterm_preview_erase(&mut self) {
if !self.preview.terminal_images.protocol.is_raster() {
return;
}
if let Some(area) = self.displayed_static_image_clear_area() {
geometry::push_unique_rect(&mut self.preview.terminal_images.pending_iterm_erase, area);
}
if let Some(area) = self.displayed_pdf_overlay_area() {
geometry::push_unique_rect(&mut self.preview.terminal_images.pending_iterm_erase, area);
}
}
pub(crate) fn preview_uses_image_overlay(&self) -> bool {
self.displayed_static_image_replaces_preview()
|| self.displayed_pdf_overlay_matches_active()
}
pub(crate) fn preview_prefers_image_surface(&self) -> bool {
self.preview_prefers_static_image_surface() || self.preview_prefers_pdf_surface()
}
fn refresh_terminal_image_window_size(&mut self) {
self.preview.terminal_images.window = (self.preview.terminal_images.protocol
!= ImageProtocol::None)
.then(query_terminal_window_size)
.flatten();
}
}
pub(in crate::app) fn place_terminal_image(
protocol: ImageProtocol,
path: &Path,
area: Rect,
excluded: &[Rect],
inline_payload: Option<&str>,
window_size: Option<TerminalWindowSize>,
) -> Result<Vec<u8>> {
match protocol {
ImageProtocol::KittyGraphics => {
kitty::place_terminal_image_with_kitty_protocol(path, area, excluded)
}
ImageProtocol::KonsoleGraphics => {
konsole::place_terminal_image_with_konsole_protocol(path, area)
}
ImageProtocol::ItermInline => {
iterm::place_terminal_image_with_iterm_protocol(path, area, inline_payload)
}
ImageProtocol::Sixel => {
let ws = window_size.ok_or_else(|| {
anyhow::anyhow!("sixel protocol requires terminal window size, but none available")
})?;
sixel::place_terminal_image_with_sixel_protocol(path, area, ws)
}
ImageProtocol::None => Ok(Vec::new()),
}
}
pub(in crate::app) fn clear_terminal_images(protocol: ImageProtocol) -> Result<Vec<u8>> {
match protocol {
ImageProtocol::KittyGraphics => kitty::clear_terminal_images_with_kitty_protocol(),
ImageProtocol::KonsoleGraphics => konsole::clear_terminal_images_with_konsole_protocol(),
ImageProtocol::ItermInline | ImageProtocol::None => Ok(Vec::new()),
ImageProtocol::Sixel => sixel::clear_terminal_images_with_sixel_protocol(),
}
}