Initial attempt at supporting new backend for kitty images

This commit is contained in:
itsjunetime
2025-05-31 17:22:36 -06:00
parent 777705b902
commit b09ce88d9f
5 changed files with 251 additions and 29 deletions
Generated
+36
View File
@@ -1518,6 +1518,22 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
] ]
[[package]]
name = "kittage"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"crossterm",
"futures-core",
"image",
"memchr",
"memmap2",
"psx-shm",
"rustix 1.0.8",
"thiserror 2.0.12",
"tokio",
]
[[package]] [[package]]
name = "kqueue" name = "kqueue"
version = "1.1.1" version = "1.1.1"
@@ -1698,6 +1714,15 @@ version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memmap2"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "483758ad303d734cec05e5c12b41d7e93e6a6390c5e9dae6bdeb7c1259012d28"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memmem" name = "memmem"
version = "0.1.1" version = "0.1.1"
@@ -2291,6 +2316,15 @@ dependencies = [
"prost", "prost",
] ]
[[package]]
name = "psx-shm"
version = "0.1.1"
source = "git+https://github.com/itsjunetime/psx-shm.git#3fcbae91217cd50ea0e4c838276ef7500cccf024"
dependencies = [
"memmap2",
"rustix 1.0.8",
]
[[package]] [[package]]
name = "quick-error" name = "quick-error"
version = "2.0.1" version = "2.0.1"
@@ -2907,6 +2941,8 @@ dependencies = [
"futures-util", "futures-util",
"image", "image",
"itertools 0.14.0", "itertools 0.14.0",
"kittage",
"memmap2",
"mimalloc", "mimalloc",
"mupdf", "mupdf",
"nix 0.30.1", "nix 0.30.1",
+2
View File
@@ -40,6 +40,8 @@ mimalloc = "0.1.43"
nix = { version = "0.30.0", features = ["signal"] } nix = { version = "0.30.0", features = ["signal"] }
mupdf = { version = "0.5.0", default-features = false, features = ["svg", "system-fonts", "img"] } mupdf = { version = "0.5.0", default-features = false, features = ["svg", "system-fonts", "img"] }
rayon = { version = "*", default-features = false } rayon = { version = "*", default-features = false }
kittage = { path = "../kittage/", features = ["crossterm-tokio", "image-crate"] }
memmap2 = "*"
# for tracing with tokio-console # for tracing with tokio-console
console-subscriber = { version = "0.4.0", optional = true } console-subscriber = { version = "0.4.0", optional = true }
+72 -6
View File
@@ -1,15 +1,42 @@
use std::num::NonZeroU32;
use flume::{Receiver, SendError, Sender, TryRecvError}; use flume::{Receiver, SendError, Sender, TryRecvError};
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use image::DynamicImage; use image::DynamicImage;
use itertools::Itertools; use itertools::Itertools;
use kittage::NumberOrId;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use ratatui_image::{Resize, picker::Picker, protocol::Protocol}; use ratatui_image::{
Resize,
picker::{Picker, ProtocolType},
protocol::Protocol
};
use rayon::iter::ParallelIterator; use rayon::iter::ParallelIterator;
use crate::renderer::{PageInfo, RenderError, fill_default}; use crate::renderer::{PageInfo, RenderError, fill_default};
#[derive(Debug)]
pub enum MaybeTransferred {
NotYet(kittage::image::Image<'static>, memmap2::MmapMut),
Transferred(kittage::ImageId)
}
pub enum ConvertedImage {
Generic(Protocol),
Kitty { img: MaybeTransferred, area: Rect }
}
impl ConvertedImage {
pub fn area(&self) -> Rect {
match self {
Self::Generic(prot) => prot.area(),
Self::Kitty { img: _, area } => *area
}
}
}
pub struct ConvertedPage { pub struct ConvertedPage {
pub page: Protocol, pub page: ConvertedImage,
pub num: usize, pub num: usize,
pub num_results: usize pub num_results: usize
} }
@@ -28,13 +55,15 @@ pub async fn run_conversion_loop(
) -> Result<(), SendError<Result<ConvertedPage, RenderError>>> { ) -> Result<(), SendError<Result<ConvertedPage, RenderError>>> {
let mut images = vec![]; let mut images = vec![];
let mut page: usize = 0; let mut page: usize = 0;
let pid = std::process::id();
fn next_page( fn next_page(
images: &mut [Option<PageInfo>], images: &mut [Option<PageInfo>],
picker: &mut Picker, picker: &mut Picker,
page: usize, page: usize,
iteration: &mut usize, iteration: &mut usize,
prerender: usize prerender: usize,
pid: u32
) -> Result<Option<ConvertedPage>, RenderError> { ) -> Result<Option<ConvertedPage>, RenderError> {
if images.is_empty() || *iteration >= prerender { if images.is_empty() || *iteration >= prerender {
return Ok(None); return Ok(None);
@@ -85,13 +114,43 @@ pub async fn run_conversion_loop(
// verified (with the ImageSurface stuff) that the image is the correct // verified (with the ImageSurface stuff) that the image is the correct
// size for the area given, so to save ratatui the work of having to // size for the area given, so to save ratatui the work of having to
// resize it, we tell them to crop it to fit. // resize it, we tell them to crop it to fit.
let txt_img = picker let txt_img = match picker.protocol_type() {
ProtocolType::Kitty => {
let area = ratatui_image::protocol::ImageSource::round_pixel_size_to_cells(
dyn_img.width(),
dyn_img.height(),
picker.font_size()
);
match kittage::image::Image::shm_from(
dyn_img,
format!("__tdf_kittage_{pid}_page_{page}").into()
) {
Ok((mut img, map)) => {
img.num_or_id = NumberOrId::Id(NonZeroU32::new(page as u32 + 1).unwrap());
ConvertedImage::Kitty {
img: MaybeTransferred::NotYet(img, map),
area
}
}
// todo: fallback to non-shm image here without cloning dyn_img above
// Err(_) => ConvertedImage::Kitty(dyn_img.into())
Err(e) =>
return Err(RenderError::Converting(format!(
"Couldn't write to shm: {e}"
))),
}
}
_ => ConvertedImage::Generic(
picker
.new_protocol(dyn_img, img_area, Resize::None) .new_protocol(dyn_img, img_area, Resize::None)
.map_err(|e| { .map_err(|e| {
RenderError::Converting(format!( RenderError::Converting(format!(
"Couldn't convert DynamicImage to ratatui image: {e}" "Couldn't convert DynamicImage to ratatui image: {e}"
)) ))
})?; })?
)
};
// update the iteration to the iteration that we stole this image from // update the iteration to the iteration that we stole this image from
*iteration = new_iter; *iteration = new_iter;
@@ -130,7 +189,14 @@ pub async fn run_conversion_loop(
Err(TryRecvError::Disconnected) => return Ok(()) Err(TryRecvError::Disconnected) => return Ok(())
} }
match next_page(&mut images, &mut picker, page, &mut iteration, prerender) { match next_page(
&mut images,
&mut picker,
page,
&mut iteration,
prerender,
pid
) {
Ok(None) => break, Ok(None) => break,
Ok(Some(img)) => sender.send(Ok(img))?, Ok(Some(img)) => sender.send(Ok(img))?,
Err(e) => sender.send(Err(e))? Err(e) => sender.send(Err(e))?
+93 -5
View File
@@ -2,8 +2,8 @@ use core::error::Error;
use std::{ use std::{
borrow::Cow, borrow::Cow,
ffi::OsString, ffi::OsString,
io::{BufReader, Read, Stdout, Write, stdout}, io::{BufReader, Read, StdoutLock, Write, stdout},
num::NonZeroUsize, num::{NonZeroU32, NonZeroUsize},
path::PathBuf path::PathBuf
}; };
@@ -17,12 +17,20 @@ use crossterm::{
}; };
use flume::{Sender, r#async::RecvStream}; use flume::{Sender, r#async::RecvStream};
use futures_util::{FutureExt, stream::StreamExt}; use futures_util::{FutureExt, stream::StreamExt};
use kittage::{
ImageDimensions, PixelFormat,
action::Action,
display::{DisplayConfig, DisplayLocation},
error::TransmitError,
image::Image as KImage,
medium::Medium
};
use notify::{Event, EventKind, RecursiveMode, Watcher}; use notify::{Event, EventKind, RecursiveMode, Watcher};
use ratatui::{Terminal, backend::CrosstermBackend}; use ratatui::{Terminal, backend::CrosstermBackend};
use ratatui_image::picker::Picker; use ratatui_image::picker::Picker;
use tdf::{ use tdf::{
PrerenderLimit, PrerenderLimit,
converter::{ConvertedPage, ConverterMsg, run_conversion_loop}, converter::{ConvertedPage, ConverterMsg, MaybeTransferred, run_conversion_loop},
renderer::{self, RenderError, RenderInfo, RenderNotif}, renderer::{self, RenderError, RenderInfo, RenderNotif},
tui::{BottomMessage, InputAction, MessageSetting, Tui} tui::{BottomMessage, InputAction, MessageSetting, Tui}
}; };
@@ -373,10 +381,90 @@ async fn enter_redraw_loop(
} }
if needs_redraw { if needs_redraw {
let mut to_display = vec![];
term.draw(|f| { term.draw(|f| {
tui.render(f, &main_area); to_display = tui.render(f, &main_area);
})?; })?;
execute!(stdout(), EndSynchronizedUpdate)?;
let mut stdout = stdout().lock();
let mut maybe_err = Ok(());
for (img, area) in to_display {
let config = DisplayConfig {
location: DisplayLocation {
x: area.x.into(),
y: area.y.into(),
..DisplayLocation::default()
},
..DisplayConfig::default()
};
maybe_err = match img {
MaybeTransferred::NotYet(image, _map) => {
let mut fake_image = KImage {
num_or_id: image.num_or_id,
format: PixelFormat::Rgb24(
ImageDimensions {
width: 0,
height: 0
},
None
),
medium: Medium::Direct {
chunk_size: None,
data: (&[]).into()
}
};
std::mem::swap(image, &mut fake_image);
let res = Action::TransmitAndDisplay {
image: fake_image,
config,
placement_id: None
}
.execute_async(&mut stdout, &mut ev_stream)
.await;
match res {
Ok((_, img_id)) => {
// We need the `_map` to be dropped here, but can't explicitly carry it
// over to here. So we're just relying on the overwrite of `img` to
// drop `_map` (and thus unmap the memory) for us
*img = MaybeTransferred::Transferred(img_id);
Ok(())
}
Err(e) => Err(match e {
TransmitError::Writing(
Action::TransmitAndDisplay {
image: failed_img, ..
},
e
) => {
*image = failed_img;
e.to_string()
}
_ => e.to_string()
})
}
}
MaybeTransferred::Transferred(image_id) => Action::Display {
image_id: *image_id,
placement_id: NonZeroU32::new(1).unwrap(),
config
}
.execute_async(&mut stdout, &mut ev_stream)
.await
.map(|(_, _)| ())
.map_err(|e| e.to_string())
};
}
if let Err(e) = maybe_err {
tui.set_msg(MessageSetting::Some(BottomMessage::Error(format!(
"Couldn't transfer image to the terminal: {e}"
))));
}
execute!(&mut stdout, EndSynchronizedUpdate)?;
} }
} }
} }
+40 -10
View File
@@ -20,9 +20,10 @@ use ratatui::{
text::{Span, Text}, text::{Span, Text},
widgets::{Block, Borders, Clear, Padding} widgets::{Block, Borders, Clear, Padding}
}; };
use ratatui_image::{Image, protocol::Protocol}; use ratatui_image::Image;
use crate::{ use crate::{
converter::{ConvertedImage, MaybeTransferred},
renderer::{RenderError, fill_default}, renderer::{RenderError, fill_default},
skip::Skip skip::Skip
}; };
@@ -74,7 +75,7 @@ struct PageConstraints {
#[derive(Default)] #[derive(Default)]
struct RenderedInfo { struct RenderedInfo {
// The image, if it has been rendered by `Converter` to that struct // The image, if it has been rendered by `Converter` to that struct
img: Option<Protocol>, img: Option<ConvertedImage>,
// The number of results for the current search term that have been found on this page. None if // The number of results for the current search term that have been found on this page. None if
// we haven't checked this page yet // we haven't checked this page yet
// Also this isn't the most efficient representation of this value, but it's accurate, so like // Also this isn't the most efficient representation of this value, but it's accurate, so like
@@ -127,10 +128,15 @@ impl Tui {
} }
// TODO: Make a way to fill the width of the screen with one page and scroll down to view it // TODO: Make a way to fill the width of the screen with one page and scroll down to view it
pub fn render(&mut self, frame: &mut Frame<'_>, full_layout: &RenderLayout) { #[must_use]
pub fn render<'s>(
&'s mut self,
frame: &mut Frame<'_>,
full_layout: &RenderLayout
) -> Vec<(&'s mut MaybeTransferred, Rect)> {
if self.showing_help_msg { if self.showing_help_msg {
self.render_help_msg(frame); self.render_help_msg(frame);
return; return vec![];
} }
if let Some((top_area, bottom_area)) = full_layout.top_and_bottom { if let Some((top_area, bottom_area)) = full_layout.top_and_bottom {
@@ -237,6 +243,7 @@ impl Tui {
// be written and set to skip it so that ratatui doesn't spend a lot of time diffing it // be written and set to skip it so that ratatui doesn't spend a lot of time diffing it
// each re-render // each re-render
frame.render_widget(Skip::new(true), img_area); frame.render_widget(Skip::new(true), img_area);
vec![]
} else { } else {
// here we calculate how many pages can fit in the available area. // here we calculate how many pages can fit in the available area.
let mut test_area_w = img_area.width; let mut test_area_w = img_area.width;
@@ -254,7 +261,7 @@ impl Tui {
take take
}) })
// and map it to their width (in cells on the terminal, not pixels) // and map it to their width (in cells on the terminal, not pixels)
.filter_map(|(_, page)| page.img.as_mut().map(|img| (img.rect().width, img))) .filter_map(|(_, page)| page.img.as_mut().map(|img| (img.area().width, img)))
// and then take them as long as they won't overflow the available area. // and then take them as long as they won't overflow the available area.
.take_while(|(width, _)| match test_area_w.checked_sub(*width) { .take_while(|(width, _)| match test_area_w.checked_sub(*width) {
Some(new_val) => { Some(new_val) => {
@@ -272,6 +279,7 @@ impl Tui {
if page_widths.is_empty() { if page_widths.is_empty() {
// If none are ready to render, just show the loading thing // If none are ready to render, just show the loading thing
Self::render_loading_in(frame, img_area); Self::render_loading_in(frame, img_area);
vec![]
} else { } else {
execute!(stdout(), BeginSynchronizedUpdate).unwrap(); execute!(stdout(), BeginSynchronizedUpdate).unwrap();
@@ -283,20 +291,42 @@ impl Tui {
self.last_render.unused_width = unused_width; self.last_render.unused_width = unused_width;
img_area.x += unused_width / 2; img_area.x += unused_width / 2;
for (width, img) in page_widths { let to_display = page_widths
.into_iter()
.filter_map(|(width, img)| {
let maybe_img =
Self::render_single_page(frame, img, Rect { width, ..img_area }); Self::render_single_page(frame, img, Rect { width, ..img_area });
img_area.x += width; img_area.x += width;
} maybe_img
})
.collect::<Vec<_>>();
// we want to set this at the very end so it doesn't get set somewhere halfway through and // 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 // then the whole diffing thing messes it up
self.last_render.rect = size; self.last_render.rect = size;
to_display
} }
} }
} }
fn render_single_page(frame: &mut Frame<'_>, page_img: &mut Protocol, img_area: Rect) { fn render_single_page<'img>(
frame: &mut Frame<'_>,
page_img: &'img mut ConvertedImage,
img_area: Rect
) -> Option<(&'img mut MaybeTransferred, Rect)> {
match page_img {
ConvertedImage::Generic(page_img) => {
frame.render_widget(Image::new(page_img), img_area); frame.render_widget(Image::new(page_img), img_area);
None
}
ConvertedImage::Kitty { img, area } => Some((img, Rect {
x: img_area.x,
y: img_area.y,
width: area.width,
height: area.height
}))
}
} }
fn render_loading_in(frame: &mut Frame<'_>, area: Rect) { fn render_loading_in(frame: &mut Frame<'_>, area: Rect) {
@@ -344,7 +374,7 @@ impl Tui {
self.page = self.page.min(n_pages - 1); self.page = self.page.min(n_pages - 1);
} }
pub fn page_ready(&mut self, img: Protocol, page_num: usize, num_results: usize) { pub fn page_ready(&mut self, img: ConvertedImage, page_num: usize, num_results: usize) {
// If this new image woulda fit within the available space on the last render AND it's // If this new image woulda fit within the available space on the last render AND it's
// within the range where it might've been rendered with the last shown pages, then reset // within the range where it might've been rendered with the last shown pages, then reset
// the last rect marker so that all images are forced to redraw on next render and this one // the last rect marker so that all images are forced to redraw on next render and this one
@@ -352,7 +382,7 @@ impl Tui {
if page_num >= self.page && page_num <= self.page + self.last_render.pages_shown { if page_num >= self.page && page_num <= self.page + self.last_render.pages_shown {
self.last_render.rect = Rect::default(); self.last_render.rect = Rect::default();
} else { } else {
let img_w = img.rect().width; let img_w = img.area().width;
if img_w <= self.last_render.unused_width { if img_w <= self.last_render.unused_width {
let num_fit = self.last_render.unused_width / img_w; let num_fit = self.last_render.unused_width / img_w;
if page_num >= self.page && (self.page + num_fit as usize) >= page_num { if page_num >= self.page && (self.page + num_fit as usize) >= page_num {