From 93cf380aadb725e27c4c820f0176530f650aed77 Mon Sep 17 00:00:00 2001 From: itsjunetime Date: Sun, 8 Feb 2026 23:43:56 -0600 Subject: [PATCH 1/2] Maybe fixed tmux for kitty --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/kitty.rs | 30 ++++++++++++++++++++++-------- src/main.rs | 8 ++++++-- src/tui.rs | 7 ++++--- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d65184..d891338 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2391,7 +2391,7 @@ dependencies = [ [[package]] name = "ratatui-image" version = "10.0.5" -source = "git+https://github.com/itsjunetime/ratatui-image.git?rev=a276a87cb8e2976442c6cc59db831db81551da89#a276a87cb8e2976442c6cc59db831db81551da89" +source = "git+https://github.com/itsjunetime/ratatui-image.git?rev=8ad154a219c1e4832a378bd07aaf39b3ddff959b#8ad154a219c1e4832a378bd07aaf39b3ddff959b" dependencies = [ "base64-simd", "icy_sixel", diff --git a/Cargo.toml b/Cargo.toml index 9474afc..e64dabb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ name = "tdf" ratatui = { git = "https://github.com/itsjunetime/ratatui.git", rev = "720ac2d0cad1ac6424364fea74856fec9c100cb1", default-features = false, features = [ "crossterm", "layout-cache" ] } # ratatui = { path = "./ratatui/ratatui/" } # We're using this to have the vb64 feature (for faster base64 encoding, since that does take up a good bit of time when converting images to the `Protocol`. It also just includes a few more features that I'm waiting on main to upstream -ratatui-image = { git = "https://github.com/itsjunetime/ratatui-image.git", rev = "a276a87cb8e2976442c6cc59db831db81551da89", default-features = false } +ratatui-image = { git = "https://github.com/itsjunetime/ratatui-image.git", rev = "8ad154a219c1e4832a378bd07aaf39b3ddff959b", default-features = false } # ratatui-image = { path = "./ratatui-image", default-features = false } crossterm = { version = "0.29.0", features = ["event-stream"] } # crossterm = { path = "../crossterm", features = ["event-stream"] } diff --git a/src/kitty.rs b/src/kitty.rs index 3d8a636..88ee632 100644 --- a/src/kitty.rs +++ b/src/kitty.rs @@ -15,7 +15,8 @@ use kittage::{ display::{CursorMovementPolicy, DisplayConfig, DisplayLocation}, error::TransmitError, image::Image, - medium::Medium + medium::Medium, + tmux::TmuxWriter }; use ratatui::layout::Position; use smallvec::SmallVec; @@ -32,7 +33,7 @@ pub struct KittyReadyToDisplay<'tui> { pub enum KittyDisplay<'tui> { NoChange, ClearImages, - DisplayImages(Vec>) + DisplayImages(SmallVec<[KittyReadyToDisplay<'tui>; 1]>) } pub struct DbgWriter { @@ -64,6 +65,7 @@ impl Write for DbgWriter { pub async fn run_action<'es>( action: Action<'_, '_>, + is_tmux: bool, ev_stream: &'es mut EventStream ) -> Result, TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>> { let writer = DbgWriter { @@ -71,13 +73,21 @@ pub async fn run_action<'es>( #[cfg(debug_assertions)] buf: String::new() }; - action - .execute_async(writer, ev_stream) - .await - .map(|(_, i)| i) + + if is_tmux { + action + .execute_async(TmuxWriter::new(writer), ev_stream) + .await + .map(|(_, i)| i) + } else { + action + .execute_async(writer, ev_stream) + .await + .map(|(_, i)| i) + } } -pub async fn do_shms_work(ev_stream: &mut EventStream) -> bool { +pub async fn do_shms_work(is_tmux: bool, ev_stream: &mut EventStream) -> bool { let img = DynamicImage::new_rgb8(1, 1); let pid = std::process::id(); let shm_name = format!("tdf_test_{pid}"); @@ -94,7 +104,7 @@ pub async fn do_shms_work(ev_stream: &mut EventStream) -> bool { enable_raw_mode().unwrap(); - let res = run_action(Action::Query(&k_img), ev_stream).await; + let res = run_action(Action::Query(&k_img), is_tmux, ev_stream).await; disable_raw_mode().unwrap(); @@ -139,6 +149,7 @@ impl Display for DisplayErrSource<'_> { pub async fn display_kitty_images<'es>( display: KittyDisplay<'_>, + is_tmux: bool, ev_stream: &'es mut EventStream ) -> Result<(), DisplayErr<'es>> { let images = match display { @@ -149,6 +160,7 @@ pub async fn display_kitty_images<'es>( effect: ClearOrDelete::Clear, which: WhichToDelete::All }), + is_tmux, ev_stream ) .await @@ -205,6 +217,7 @@ pub async fn display_kitty_images<'es>( config, placement_id: None }, + is_tmux, ev_stream ) .await; @@ -224,6 +237,7 @@ pub async fn display_kitty_images<'es>( placement_id: *image_id, config }, + is_tmux, ev_stream ) .await diff --git a/src/main.rs b/src/main.rs index 5d7c806..8d6f417 100644 --- a/src/main.rs +++ b/src/main.rs @@ -298,8 +298,9 @@ async fn inner_main() -> Result<(), WrappedErr> { let (to_main, from_converter) = flume::unbounded(); let is_kitty = picker.protocol_type() == ProtocolType::Kitty; + let is_tmux = picker.is_tmux(); - let shms_work = is_kitty && do_shms_work(&mut ev_stream).await; + let shms_work = is_kitty && do_shms_work(is_tmux, &mut ev_stream).await; tokio::spawn(run_conversion_loop( to_main, from_main, picker, 20, shms_work @@ -329,6 +330,7 @@ async fn inner_main() -> Result<(), WrappedErr> { effect: ClearOrDelete::Delete, which: WhichToDelete::IdRange(NonZeroU32::new(1).unwrap()..=NonZeroU32::MAX) }), + is_tmux, &mut ev_stream ) .await @@ -357,6 +359,7 @@ async fn inner_main() -> Result<(), WrappedErr> { to_converter, from_converter, fullscreen, + is_tmux, tui, &mut term, main_area, @@ -385,6 +388,7 @@ async fn enter_redraw_loop( to_converter: Sender, mut from_converter: RecvStream<'_, Result>, mut fullscreen: bool, + is_tmux: bool, mut tui: Tui, term: &mut Terminal>, mut main_area: tdf::tui::RenderLayout, @@ -462,7 +466,7 @@ async fn enter_redraw_loop( to_display = tui.render(f, &main_area, font_size); })?; - let maybe_err = display_kitty_images(to_display, &mut ev_stream).await; + let maybe_err = display_kitty_images(to_display, is_tmux, &mut ev_stream).await; if let Err(DisplayErr { failed_pages, diff --git a/src/tui.rs b/src/tui.rs index 4e39f2b..7f6f1e8 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -19,6 +19,7 @@ use ratatui::{ widgets::{Block, Borders, Clear, Padding, Paragraph, Wrap} }; use ratatui_image::{FontSize, Image}; +use smallvec::SmallVec; use crate::{ FitOrFill, @@ -329,7 +330,7 @@ impl Tui { .cell_pan_from_top .min(img_cell_h.saturating_sub(img_section_h.ceil() as u16)); - KittyDisplay::DisplayImages(vec![KittyReadyToDisplay { + KittyDisplay::DisplayImages(SmallVec::from([KittyReadyToDisplay { img, page_num, pos: Position { @@ -345,7 +346,7 @@ impl Tui { rows: img_area.height, ..DisplayLocation::default() } - }]) + }])) } #[must_use] @@ -487,7 +488,7 @@ impl Tui { display_loc: DisplayLocation::default() }) }) - .collect::>(); + .collect::>(); // we want to set this at the very end so it doesn't get set somewhere halfway through and // then the whole diffing thing messes it up From 1bf38e4d18f8c944ce3ea8b28016650f177bf4e2 Mon Sep 17 00:00:00 2001 From: itsjunetime Date: Sun, 19 Apr 2026 12:56:17 -0500 Subject: [PATCH 2/2] fix images not disappearing after terminal exits --- src/kitty.rs | 71 ++++++++++++++++++++++++++++++++++++++-------------- src/main.rs | 58 ++++++++++++++++++++++++------------------ 2 files changed, 86 insertions(+), 43 deletions(-) diff --git a/src/kitty.rs b/src/kitty.rs index 88ee632..f14dbbb 100644 --- a/src/kitty.rs +++ b/src/kitty.rs @@ -1,5 +1,8 @@ use core::fmt::Display; -use std::{io::Write, num::NonZeroU32}; +use std::{ + io::{StdoutLock, Write, stdout}, + num::NonZeroU32 +}; use crossterm::{ cursor::MoveTo, @@ -63,28 +66,55 @@ impl Write for DbgWriter { } } +pub enum MaybeTmuxWriter +where + W: Write +{ + Tmux(TmuxWriter), + Normal(W) +} + +impl MaybeTmuxWriter { + pub fn new(w: W, is_tmux: bool) -> Self { + if is_tmux { + Self::Tmux(TmuxWriter::new(w)) + } else { + Self::Normal(w) + } + } +} + +impl Write for &mut MaybeTmuxWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + match *self { + MaybeTmuxWriter::Tmux(t) => t.write(buf), + MaybeTmuxWriter::Normal(w) => w.write(buf) + } + } + + fn flush(&mut self) -> std::io::Result<()> { + match *self { + MaybeTmuxWriter::Tmux(t) => t.flush(), + MaybeTmuxWriter::Normal(w) => w.flush() + } + } +} + pub async fn run_action<'es>( action: Action<'_, '_>, - is_tmux: bool, + writer: &mut MaybeTmuxWriter>, ev_stream: &'es mut EventStream ) -> Result, TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>> { let writer = DbgWriter { - w: std::io::stdout().lock(), + w: writer, #[cfg(debug_assertions)] buf: String::new() }; - if is_tmux { - action - .execute_async(TmuxWriter::new(writer), ev_stream) - .await - .map(|(_, i)| i) - } else { - action - .execute_async(writer, ev_stream) - .await - .map(|(_, i)| i) - } + action + .execute_async(writer, ev_stream) + .await + .map(|(_, i)| i) } pub async fn do_shms_work(is_tmux: bool, ev_stream: &mut EventStream) -> bool { @@ -104,7 +134,8 @@ pub async fn do_shms_work(is_tmux: bool, ev_stream: &mut EventStream) -> bool { enable_raw_mode().unwrap(); - let res = run_action(Action::Query(&k_img), is_tmux, ev_stream).await; + let mut writer = MaybeTmuxWriter::new(stdout().lock(), is_tmux); + let res = run_action(Action::Query(&k_img), &mut writer, ev_stream).await; disable_raw_mode().unwrap(); @@ -152,6 +183,8 @@ pub async fn display_kitty_images<'es>( is_tmux: bool, ev_stream: &'es mut EventStream ) -> Result<(), DisplayErr<'es>> { + let mut writer = MaybeTmuxWriter::new(stdout().lock(), is_tmux); + let images = match display { KittyDisplay::NoChange => return Ok(()), KittyDisplay::DisplayImages(_) | KittyDisplay::ClearImages => { @@ -160,7 +193,7 @@ pub async fn display_kitty_images<'es>( effect: ClearOrDelete::Clear, which: WhichToDelete::All }), - is_tmux, + &mut writer, ev_stream ) .await @@ -188,7 +221,7 @@ pub async fn display_kitty_images<'es>( ..DisplayConfig::default() }; - execute!(std::io::stdout(), MoveTo(pos.x, pos.y)).unwrap(); + execute!(&mut writer, MoveTo(pos.x, pos.y)).unwrap(); log::debug!("going to display img {img:#?}"); log::debug!("displaying with config {config:#?}"); @@ -217,7 +250,7 @@ pub async fn display_kitty_images<'es>( config, placement_id: None }, - is_tmux, + &mut writer, ev_stream ) .await; @@ -237,7 +270,7 @@ pub async fn display_kitty_images<'es>( placement_id: *image_id, config }, - is_tmux, + &mut writer, ev_stream ) .await diff --git a/src/main.rs b/src/main.rs index 8d6f417..080e52a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,4 @@ -use core::{ - error::Error, - num::{NonZeroU32, NonZeroUsize} -}; +use core::{error::Error, num::NonZeroUsize}; use std::{ borrow::Cow, ffi::OsString, @@ -16,8 +13,8 @@ use crossterm::{ event::EventStream, execute, terminal::{ - EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, - enable_raw_mode, window_size + EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen, WindowSize, + disable_raw_mode, enable_raw_mode, window_size } }; use debounce::EventDebouncer; @@ -39,7 +36,8 @@ use tdf::{ PrerenderLimit, converter::{ConvertedPage, ConverterMsg, run_conversion_loop}, kitty::{ - DisplayErr, DisplayErrSource, KittyDisplay, display_kitty_images, do_shms_work, run_action + DisplayErr, DisplayErrSource, KittyDisplay, MaybeTmuxWriter, display_kitty_images, + do_shms_work, run_action }, renderer::{self, MUPDF_BLACK, MUPDF_WHITE, RenderError, RenderInfo, RenderNotif}, tui::{BottomMessage, InputAction, MessageSetting, Tui} @@ -250,22 +248,20 @@ async fn inner_main() -> Result<(), WrappedErr> { // We need to create `picker` on this thread because if we create it on the `renderer` thread, // it messes up something with user input. Input never makes it to the crossterm thing let picker = Picker::from_query_stdio() - .or_else(|e| match e { - ratatui_image::errors::Errors::NoFontSize if - window_size.width != 0 - && window_size.height != 0 - && window_size.columns != 0 - && window_size.rows != 0 => - { + .or_else(|e| match (e, window_size) { + ( + ratatui_image::errors::Errors::NoFontSize, + WindowSize { width: 1.., height: 1.., columns: 1.., rows: 1.. } + ) => { // the 'equivalent' that is suggested instead is not the same. We need to keep // calling this. #[expect(deprecated)] Ok(Picker::from_fontsize((cell_width_px, cell_height_px))) }, - ratatui_image::errors::Errors::NoFontSize => Err(WrappedErr( + (ratatui_image::errors::Errors::NoFontSize, _) => Err(WrappedErr( "Unable to detect your terminal's font size; this is an issue with your terminal emulator.\nPlease use a different terminal emulator or report this bug to tdf.".into() )), - e => Err(WrappedErr(format!("Couldn't get the necessary information to set up images: {e}").into())) + (e, _) => Err(WrappedErr(format!("Couldn't get the necessary information to set up images: {e}").into())) })?; // then we want to spawn off the rendering task @@ -325,12 +321,13 @@ async fn inner_main() -> Result<(), WrappedErr> { })?; if is_kitty { + let mut writer = MaybeTmuxWriter::new(stdout().lock(), is_tmux); run_action( Action::Delete(DeleteConfig { effect: ClearOrDelete::Delete, - which: WhichToDelete::IdRange(NonZeroU32::new(1).unwrap()..=NonZeroU32::MAX) + which: WhichToDelete::All }), - is_tmux, + &mut writer, &mut ev_stream ) .await @@ -352,8 +349,8 @@ async fn inner_main() -> Result<(), WrappedErr> { let tui_rx = tui_rx.into_stream(); let from_converter = from_converter.into_stream(); - enter_redraw_loop( - ev_stream, + let res = enter_redraw_loop( + &mut ev_stream, to_renderer, tui_rx, to_converter, @@ -373,16 +370,29 @@ async fn inner_main() -> Result<(), WrappedErr> { ) .into() ) - })?; + }); + if is_kitty { + let mut writer = MaybeTmuxWriter::new(stdout().lock(), is_tmux); + _ = run_action( + Action::Delete(DeleteConfig { + effect: ClearOrDelete::Delete, + which: WhichToDelete::All + }), + &mut writer, + &mut ev_stream + ) + .await; + } drop(maybe_logger); - Ok(()) + + res } // oh shut up clippy who cares #[expect(clippy::too_many_arguments)] async fn enter_redraw_loop( - mut ev_stream: EventStream, + ev_stream: &mut EventStream, to_renderer: Sender, mut tui_rx: RecvStream<'_, Result>, to_converter: Sender, @@ -466,7 +476,7 @@ async fn enter_redraw_loop( to_display = tui.render(f, &main_area, font_size); })?; - let maybe_err = display_kitty_images(to_display, is_tmux, &mut ev_stream).await; + let maybe_err = display_kitty_images(to_display, is_tmux, ev_stream).await; if let Err(DisplayErr { failed_pages,