mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
incorporate recovering from deleted images
This commit is contained in:
@@ -32,6 +32,8 @@ jobs:
|
|||||||
run: rustup component add clippy rustfmt
|
run: rustup component add clippy rustfmt
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
run: cargo clippy -- -D warnings
|
run: cargo clippy -- -D warnings
|
||||||
|
- name: Tests
|
||||||
|
run: cargo test
|
||||||
- name: Check fmt
|
- name: Check fmt
|
||||||
run: cargo fmt -- --check
|
run: cargo fmt -- --check
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
|||||||
Generated
-1
@@ -3015,7 +3015,6 @@ dependencies = [
|
|||||||
"flume",
|
"flume",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"image",
|
"image",
|
||||||
"itertools 0.14.0",
|
|
||||||
"kittage",
|
"kittage",
|
||||||
"log",
|
"log",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ image = { version = "0.25.1", features = ["pnm", "rayon", "png"], default-featur
|
|||||||
notify = { version = "8.0.0", features = ["crossbeam-channel"] }
|
notify = { version = "8.0.0", features = ["crossbeam-channel"] }
|
||||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
||||||
futures-util = { version = "0.3.30", default-features = false }
|
futures-util = { version = "0.3.30", default-features = false }
|
||||||
itertools = "*"
|
|
||||||
flume = { version = "0.11.0", default-features = false, features = ["async"] }
|
flume = { version = "0.11.0", default-features = false, features = ["async"] }
|
||||||
xflags = "0.4.0-pre.2"
|
xflags = "0.4.0-pre.2"
|
||||||
mimalloc = "0.1.43"
|
mimalloc = "0.1.43"
|
||||||
|
|||||||
+16
-9
@@ -1,9 +1,8 @@
|
|||||||
use std::num::NonZeroU32;
|
use std::num::{NonZeroU32, NonZeroUsize};
|
||||||
|
|
||||||
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 kittage::NumberOrId;
|
use kittage::NumberOrId;
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
use ratatui_image::{
|
use ratatui_image::{
|
||||||
@@ -13,9 +12,11 @@ use ratatui_image::{
|
|||||||
};
|
};
|
||||||
use rayon::iter::ParallelIterator;
|
use rayon::iter::ParallelIterator;
|
||||||
|
|
||||||
use crate::renderer::{PageInfo, RenderError, fill_default};
|
use crate::{
|
||||||
|
renderer::{PageInfo, RenderError, fill_default},
|
||||||
|
skip::InterleavedAroundWithMax
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum MaybeTransferred {
|
pub enum MaybeTransferred {
|
||||||
NotYet(kittage::image::Image<'static>),
|
NotYet(kittage::image::Image<'static>),
|
||||||
Transferred(kittage::ImageId)
|
Transferred(kittage::ImageId)
|
||||||
@@ -74,13 +75,19 @@ pub async fn run_conversion_loop(
|
|||||||
let idx_start = page.saturating_sub(prerender / 2);
|
let idx_start = page.saturating_sub(prerender / 2);
|
||||||
let idx_end = idx_start.saturating_add(prerender).min(images.len());
|
let idx_end = idx_start.saturating_add(prerender).min(images.len());
|
||||||
|
|
||||||
|
// If there's none to render, then why bother.
|
||||||
|
let Some(idx_end) = NonZeroUsize::new(idx_end) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
// then we go through all the indices available to us and find the first one that has an
|
// then we go through all the indices available to us and find the first one that has an
|
||||||
// image available to steal
|
// image available to steal
|
||||||
let Some((page_info, new_iter, page_num)) = (idx_start..page)
|
let Some((page_info, new_iter, page_num)) =
|
||||||
.interleave(page..idx_end)
|
InterleavedAroundWithMax::new(page, idx_start, idx_end)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
// .skip(*iteration)
|
.take(prerender)
|
||||||
.find_map(|(i_idx, p_idx)| images[p_idx].take().map(|p| (p, i_idx, p_idx)))
|
// .skip(*iteration)
|
||||||
|
.find_map(|(i_idx, p_idx)| images[p_idx].take().map(|p| (p, i_idx, p_idx)))
|
||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|||||||
+26
-29
@@ -14,7 +14,6 @@ use ratatui::prelude::Rect;
|
|||||||
|
|
||||||
use crate::converter::MaybeTransferred;
|
use crate::converter::MaybeTransferred;
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct DbgWriter<W: Write> {
|
pub struct DbgWriter<W: Write> {
|
||||||
w: W,
|
w: W,
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -44,8 +43,7 @@ impl<W: Write> Write for DbgWriter<W> {
|
|||||||
pub async fn run_action<'image, 'data, 'es>(
|
pub async fn run_action<'image, 'data, 'es>(
|
||||||
action: Action<'image, 'data>,
|
action: Action<'image, 'data>,
|
||||||
ev_stream: &'es mut EventStream
|
ev_stream: &'es mut EventStream
|
||||||
) -> Result<ImageId, TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>>
|
) -> Result<ImageId, TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>> {
|
||||||
{
|
|
||||||
let writer = DbgWriter {
|
let writer = DbgWriter {
|
||||||
w: std::io::stdout().lock(),
|
w: std::io::stdout().lock(),
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
@@ -57,14 +55,17 @@ pub async fn run_action<'image, 'data, 'es>(
|
|||||||
.map(|(_, i)| i)
|
.map(|(_, i)| i)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn display_kitty_images(
|
pub async fn display_kitty_images<'es>(
|
||||||
images: Vec<(usize, &mut MaybeTransferred, Rect)>,
|
images: Vec<(usize, &mut MaybeTransferred, Rect)>,
|
||||||
ev_stream: &mut EventStream
|
ev_stream: &'es mut EventStream
|
||||||
) -> Result<(), (Vec<usize>, String)> {
|
) -> Result<
|
||||||
if images.is_empty() {
|
(),
|
||||||
return Ok(());
|
(
|
||||||
}
|
Vec<usize>,
|
||||||
|
&'static str,
|
||||||
|
TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>
|
||||||
|
)
|
||||||
|
> {
|
||||||
run_action(
|
run_action(
|
||||||
Action::Delete(DeleteConfig {
|
Action::Delete(DeleteConfig {
|
||||||
effect: ClearOrDelete::Clear,
|
effect: ClearOrDelete::Clear,
|
||||||
@@ -73,7 +74,7 @@ pub async fn display_kitty_images(
|
|||||||
ev_stream
|
ev_stream
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (vec![], format!("Couldn't clear previous images: {e}")))?;
|
.map_err(|e| (vec![], "Couldn't clear previous images", e))?;
|
||||||
|
|
||||||
let mut err = None;
|
let mut err = None;
|
||||||
for (page_num, img, area) in images {
|
for (page_num, img, area) in images {
|
||||||
@@ -114,34 +115,30 @@ pub async fn display_kitty_images(
|
|||||||
*img = MaybeTransferred::Transferred(img_id);
|
*img = MaybeTransferred::Transferred(img_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => Err((Some(page_num), e.to_string())),
|
Err(e) => Err((page_num, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MaybeTransferred::Transferred(image_id) => {
|
MaybeTransferred::Transferred(image_id) => run_action(
|
||||||
run_action(
|
Action::Display {
|
||||||
Action::Display {
|
image_id: *image_id,
|
||||||
image_id: *image_id,
|
placement_id: *image_id,
|
||||||
placement_id: *image_id,
|
config
|
||||||
config
|
},
|
||||||
},
|
ev_stream
|
||||||
ev_stream
|
)
|
||||||
)
|
.await
|
||||||
.await
|
.map(|_| ())
|
||||||
.map(|_| ())
|
.map_err(|e| (page_num, e))
|
||||||
.map_err(|e| (None, e.to_string()))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err((id, e)) = this_err {
|
if let Err((id, e)) = this_err {
|
||||||
let e = err.get_or_insert_with(|| (vec![], e));
|
let e = err.get_or_insert_with(|| (vec![], e));
|
||||||
if let Some(id) = id {
|
e.0.push(id);
|
||||||
e.0.push(id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match err {
|
match err {
|
||||||
Some(e) => Err(e),
|
Some((replace, e)) => Err((replace, "Couldn't transfer image to the terminal", e)),
|
||||||
None => Ok(())
|
None => Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+38
-33
@@ -2,7 +2,7 @@ use core::error::Error;
|
|||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
ffi::OsString,
|
ffi::OsString,
|
||||||
io::{BufReader, Read, Write, stdout},
|
io::{stdout, BufReader, Read, Stdout, Write},
|
||||||
num::{NonZeroU32, NonZeroUsize},
|
num::{NonZeroU32, NonZeroUsize},
|
||||||
path::PathBuf
|
path::PathBuf
|
||||||
};
|
};
|
||||||
@@ -20,7 +20,8 @@ use flexi_logger::FileSpec;
|
|||||||
use futures_util::{FutureExt, stream::StreamExt};
|
use futures_util::{FutureExt, stream::StreamExt};
|
||||||
use kittage::{
|
use kittage::{
|
||||||
action::Action,
|
action::Action,
|
||||||
delete::{ClearOrDelete, DeleteConfig, WhichToDelete}
|
delete::{ClearOrDelete, DeleteConfig, WhichToDelete},
|
||||||
|
error::{TerminalError, TransmitError}
|
||||||
};
|
};
|
||||||
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
use notify::{Event, EventKind, RecursiveMode, Watcher};
|
||||||
use ratatui::{Terminal, backend::CrosstermBackend};
|
use ratatui::{Terminal, backend::CrosstermBackend};
|
||||||
@@ -103,17 +104,17 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
maybe_logger =
|
maybe_logger =
|
||||||
Some(
|
Some(
|
||||||
flexi_logger::Logger::try_with_env()
|
flexi_logger::Logger::try_with_env()
|
||||||
.map_err(|e| WrappedErr(format!("Couldn't create initial logger: {e}")))?
|
.map_err(|e| WrappedErr(format!("Couldn't create initial logger: {e}").into()))?
|
||||||
.log_to_file(FileSpec::try_from("./debug.log").map_err(|e| {
|
.log_to_file(FileSpec::try_from("./debug.log").map_err(|e| {
|
||||||
WrappedErr(format!("Couldn't create FileSpec for logger: {e}"))
|
WrappedErr(format!("Couldn't create FileSpec for logger: {e}").into())
|
||||||
})?)
|
})?)
|
||||||
.start()
|
.start()
|
||||||
.map_err(|e| WrappedErr(format!("Can't start logger: {e}")))?
|
.map_err(|e| WrappedErr(format!("Can't start logger: {e}").into()))?
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (watch_to_render_tx, render_rx) = flume::unbounded();
|
let (watch_to_render_tx, render_rx) = flume::unbounded();
|
||||||
let tui_tx = watch_to_render_tx.clone();
|
let to_renderer = watch_to_render_tx.clone();
|
||||||
|
|
||||||
let (render_tx, tui_rx) = flume::unbounded();
|
let (render_tx, tui_rx) = flume::unbounded();
|
||||||
let watch_to_tui_tx = render_tx.clone();
|
let watch_to_tui_tx = render_tx.clone();
|
||||||
@@ -247,7 +248,7 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let ev_stream = crossterm::event::EventStream::new();
|
let mut ev_stream = crossterm::event::EventStream::new();
|
||||||
|
|
||||||
let (to_converter, from_main) = flume::unbounded();
|
let (to_converter, from_main) = flume::unbounded();
|
||||||
let (to_main, from_converter) = flume::unbounded();
|
let (to_main, from_converter) = flume::unbounded();
|
||||||
@@ -294,12 +295,13 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
}),
|
}),
|
||||||
&mut ev_stream
|
&mut ev_stream
|
||||||
)
|
)
|
||||||
.await?;
|
.await
|
||||||
|
.map_err(|e| WrappedErr(format!("Couldn't delete all previous images from memory: {e}").into()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fullscreen = flags.fullscreen.unwrap_or_default();
|
let fullscreen = flags.fullscreen.unwrap_or_default();
|
||||||
let main_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
let main_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
||||||
tui_tx
|
to_renderer
|
||||||
.send(RenderNotif::Area(main_area.page_area))
|
.send(RenderNotif::Area(main_area.page_area))
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
WrappedErr(
|
WrappedErr(
|
||||||
@@ -312,7 +314,7 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
|
|
||||||
enter_redraw_loop(
|
enter_redraw_loop(
|
||||||
ev_stream,
|
ev_stream,
|
||||||
tui_tx,
|
to_renderer,
|
||||||
tui_rx,
|
tui_rx,
|
||||||
to_converter,
|
to_converter,
|
||||||
from_converter,
|
from_converter,
|
||||||
@@ -339,6 +341,8 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
disable_raw_mode().unwrap();
|
disable_raw_mode().unwrap();
|
||||||
|
|
||||||
|
drop(maybe_logger);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +350,7 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
#[expect(clippy::too_many_arguments)]
|
#[expect(clippy::too_many_arguments)]
|
||||||
async fn enter_redraw_loop(
|
async fn enter_redraw_loop(
|
||||||
mut ev_stream: EventStream,
|
mut ev_stream: EventStream,
|
||||||
tui_tx: Sender<RenderNotif>,
|
to_renderer: Sender<RenderNotif>,
|
||||||
mut tui_rx: RecvStream<'_, Result<RenderInfo, RenderError>>,
|
mut tui_rx: RecvStream<'_, Result<RenderInfo, RenderError>>,
|
||||||
to_converter: Sender<ConverterMsg>,
|
to_converter: Sender<ConverterMsg>,
|
||||||
mut from_converter: RecvStream<'_, Result<ConvertedPage, RenderError>>,
|
mut from_converter: RecvStream<'_, Result<ConvertedPage, RenderError>>,
|
||||||
@@ -370,11 +374,11 @@ async fn enter_redraw_loop(
|
|||||||
InputAction::Redraw => (),
|
InputAction::Redraw => (),
|
||||||
InputAction::QuitApp => return Ok(()),
|
InputAction::QuitApp => return Ok(()),
|
||||||
InputAction::JumpingToPage(page) => {
|
InputAction::JumpingToPage(page) => {
|
||||||
tui_tx.send(RenderNotif::JumpToPage(page))?;
|
to_renderer.send(RenderNotif::JumpToPage(page))?;
|
||||||
to_converter.send(ConverterMsg::GoToPage(page))?;
|
to_converter.send(ConverterMsg::GoToPage(page))?;
|
||||||
},
|
},
|
||||||
InputAction::Search(term) => tui_tx.send(RenderNotif::Search(term))?,
|
InputAction::Search(term) => to_renderer.send(RenderNotif::Search(term))?,
|
||||||
InputAction::Invert => tui_tx.send(RenderNotif::Invert)?,
|
InputAction::Invert => to_renderer.send(RenderNotif::Invert)?,
|
||||||
InputAction::Fullscreen => fullscreen = !fullscreen,
|
InputAction::Fullscreen => fullscreen = !fullscreen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -408,7 +412,7 @@ async fn enter_redraw_loop(
|
|||||||
let new_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
let new_area = Tui::main_layout(&term.get_frame(), fullscreen);
|
||||||
if new_area != main_area {
|
if new_area != main_area {
|
||||||
main_area = new_area;
|
main_area = new_area;
|
||||||
tui_tx.send(RenderNotif::Area(main_area.page_area))?;
|
to_renderer.send(RenderNotif::Area(main_area.page_area))?;
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,32 +422,33 @@ async fn enter_redraw_loop(
|
|||||||
to_display = tui.render(f, &main_area);
|
to_display = tui.render(f, &main_area);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let maybe_err = display_kitty_images(to_display, &mut ev_stream).await;
|
if !to_display.is_empty() {
|
||||||
|
let maybe_err = display_kitty_images(to_display, &mut ev_stream).await;
|
||||||
|
|
||||||
if let Err((to_replace, e)) = maybe_err {
|
if let Err((to_replace, err_desc, enum_err)) = maybe_err {
|
||||||
tui.set_msg(MessageSetting::Some(BottomMessage::Error(format!(
|
match enum_err {
|
||||||
"Couldn't transfer image to the terminal: {e}"
|
// This is the error that kitty provides us when it deletes an image due to
|
||||||
))));
|
// memory constraints, so if we get it, we just fix it by re-rendering and
|
||||||
|
// don't display it to the user
|
||||||
|
TransmitError::Terminal(TerminalError::NoEntity(e))
|
||||||
|
if e.contains("refers to non-existent image") =>
|
||||||
|
(),
|
||||||
|
_ => tui.set_msg(MessageSetting::Some(BottomMessage::Error(format!(
|
||||||
|
"{err_desc}: {enum_err}"
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
for page_num in to_replace {
|
for page_num in to_replace {
|
||||||
tui.page_failed_display(page_num);
|
tui.page_failed_display(page_num);
|
||||||
|
// So that they get re-rendered and sent over again
|
||||||
|
to_renderer.send(RenderNotif::PageNeedsReRender(page_num))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
execute!(stdout().lock(), EndSynchronizedUpdate)?;
|
execute!(stdout().lock(), EndSynchronizedUpdate)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
execute!(
|
|
||||||
term.backend_mut(),
|
|
||||||
LeaveAlternateScreen,
|
|
||||||
crossterm::cursor::Show
|
|
||||||
)?;
|
|
||||||
disable_raw_mode()?;
|
|
||||||
|
|
||||||
drop(maybe_logger);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_notify_ev(
|
fn on_notify_ev(
|
||||||
|
|||||||
+59
-39
@@ -1,17 +1,17 @@
|
|||||||
use std::{thread::sleep, time::Duration};
|
use std::{collections::VecDeque, num::NonZeroUsize, thread::sleep, time::Duration};
|
||||||
|
|
||||||
use flume::{Receiver, SendError, Sender, TryRecvError};
|
use flume::{Receiver, SendError, Sender, TryRecvError};
|
||||||
use itertools::Itertools;
|
|
||||||
use mupdf::{
|
use mupdf::{
|
||||||
Colorspace, Document, Matrix, Page, Pixmap, Quad, TextPageOptions, text_page::SearchHitResponse
|
Colorspace, Document, Matrix, Page, Pixmap, Quad, TextPageOptions, text_page::SearchHitResponse
|
||||||
};
|
};
|
||||||
use ratatui::layout::Rect;
|
use ratatui::layout::Rect;
|
||||||
|
|
||||||
use crate::PrerenderLimit;
|
use crate::{PrerenderLimit, skip::InterleavedAroundWithMax};
|
||||||
|
|
||||||
pub enum RenderNotif {
|
pub enum RenderNotif {
|
||||||
Area(Rect),
|
Area(Rect),
|
||||||
JumpToPage(usize),
|
JumpToPage(usize),
|
||||||
|
PageNeedsReRender(usize),
|
||||||
Search(String),
|
Search(String),
|
||||||
Reload,
|
Reload,
|
||||||
Invert
|
Invert
|
||||||
@@ -72,7 +72,7 @@ pub fn fill_default<T: Default>(vec: &mut Vec<T>, size: usize) {
|
|||||||
// We're allowing passing by value here because this is only called once, at the beginning of the
|
// We're allowing passing by value here because this is only called once, at the beginning of the
|
||||||
// program, and the arguments that 'should' be passed by value (`receiver` and `size`) would
|
// program, and the arguments that 'should' be passed by value (`receiver` and `size`) would
|
||||||
// probably be more performant if accessed by-value instead of through a reference. Probably.
|
// probably be more performant if accessed by-value instead of through a reference. Probably.
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
|
||||||
pub fn start_rendering(
|
pub fn start_rendering(
|
||||||
path: &str,
|
path: &str,
|
||||||
sender: Sender<Result<RenderInfo, RenderError>>,
|
sender: Sender<Result<RenderInfo, RenderError>>,
|
||||||
@@ -94,6 +94,8 @@ pub fn start_rendering(
|
|||||||
let mut invert = false;
|
let mut invert = false;
|
||||||
let mut preserved_area = None;
|
let mut preserved_area = None;
|
||||||
|
|
||||||
|
let mut need_rerender = VecDeque::new();
|
||||||
|
|
||||||
'reload: loop {
|
'reload: loop {
|
||||||
let doc = match Document::open(path) {
|
let doc = match Document::open(path) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -126,7 +128,13 @@ pub fn start_rendering(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let n_pages = match doc.page_count() {
|
let n_pages = match doc.page_count() {
|
||||||
Ok(n) => n as usize,
|
Ok(n) => match NonZeroUsize::new(n as usize) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => {
|
||||||
|
sleep(Duration::from_secs(1));
|
||||||
|
continue 'reload;
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
sender.send(Err(RenderError::Doc(e)))?;
|
sender.send(Err(RenderError::Doc(e)))?;
|
||||||
// just basic backoff i think
|
// just basic backoff i think
|
||||||
@@ -135,7 +143,7 @@ pub fn start_rendering(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
sender.send(Ok(RenderInfo::NumPages(n_pages)))?;
|
sender.send(Ok(RenderInfo::NumPages(n_pages.get())))?;
|
||||||
|
|
||||||
// We're using this vec of bools to indicate which page numbers have already been rendered,
|
// We're using this vec of bools to indicate which page numbers have already been rendered,
|
||||||
// to support people jumping to specific pages and having quick rendering results. We
|
// to support people jumping to specific pages and having quick rendering results. We
|
||||||
@@ -143,7 +151,7 @@ pub fn start_rendering(
|
|||||||
// doing basically nothing, but if we get a notification that something has been jumped to,
|
// doing basically nothing, but if we get a notification that something has been jumped to,
|
||||||
// then we can split at that page and render at both sides of it
|
// then we can split at that page and render at both sides of it
|
||||||
let mut rendered = Vec::new();
|
let mut rendered = Vec::new();
|
||||||
fill_default::<PrevRender>(&mut rendered, n_pages);
|
fill_default::<PrevRender>(&mut rendered, n_pages.get());
|
||||||
let mut start_point = 0;
|
let mut start_point = 0;
|
||||||
|
|
||||||
// This is kinda a weird way of doing this, but if we get a notification that the area
|
// This is kinda a weird way of doing this, but if we get a notification that the area
|
||||||
@@ -163,6 +171,9 @@ pub fn start_rendering(
|
|||||||
new_area
|
new_area
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let area_w = f32::from(area.width) * f32::from(col_w);
|
||||||
|
let area_h = f32::from(area.height) * f32::from(col_h);
|
||||||
|
|
||||||
// what we do with a notif is the same regardless of if we're in the middle of
|
// what we do with a notif is the same regardless of if we're in the middle of
|
||||||
// rendering the list of pages or we're all done
|
// rendering the list of pages or we're all done
|
||||||
macro_rules! handle_notif {
|
macro_rules! handle_notif {
|
||||||
@@ -178,13 +189,18 @@ pub fn start_rendering(
|
|||||||
}
|
}
|
||||||
RenderNotif::Area(new_area) => {
|
RenderNotif::Area(new_area) => {
|
||||||
preserved_area = Some(new_area);
|
preserved_area = Some(new_area);
|
||||||
fill_default(&mut rendered, n_pages);
|
fill_default(&mut rendered, n_pages.get());
|
||||||
continue 'render_pages;
|
continue 'render_pages;
|
||||||
}
|
}
|
||||||
RenderNotif::JumpToPage(page) => {
|
RenderNotif::JumpToPage(page) => {
|
||||||
start_point = page;
|
start_point = page;
|
||||||
continue 'render_pages;
|
continue 'render_pages;
|
||||||
}
|
}
|
||||||
|
RenderNotif::PageNeedsReRender(page) => {
|
||||||
|
rendered[page].successful = false;
|
||||||
|
need_rerender.push_back(page);
|
||||||
|
continue 'render_pages;
|
||||||
|
}
|
||||||
RenderNotif::Search(term) => {
|
RenderNotif::Search(term) => {
|
||||||
if term.is_empty() {
|
if term.is_empty() {
|
||||||
// If the term is set to nothing, then we don't need to re-render
|
// If the term is set to nothing, then we don't need to re-render
|
||||||
@@ -214,28 +230,21 @@ pub fn start_rendering(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let any_not_searched = rendered.iter().any(|r| r.num_search_found.is_none());
|
let any_not_searched = rendered.iter().any(|r| r.num_search_found.is_none());
|
||||||
let (left, right) = rendered.split_at_mut(start_point);
|
|
||||||
|
|
||||||
// This is our iterator over all the pages we want to look at and render. It uses this
|
// This is our iterator over all the pages we want to look at and render. It uses this
|
||||||
// weird 'interleave' thing to render pages on *both sides* of the currently-displayed
|
// weird 'interleave' thing to render pages on *both sides* of the currently-displayed
|
||||||
// page in case they device to go forward or backwards.
|
// page in case they device to go forward or backwards.
|
||||||
let page_iter = right
|
let page_iter = PopOnNext {
|
||||||
.iter_mut()
|
inner: &mut need_rerender
|
||||||
.enumerate()
|
}
|
||||||
.map(move |(idx, p)| (idx + start_point, p))
|
.chain(InterleavedAroundWithMax::new(start_point, 0, n_pages).take(
|
||||||
.interleave(
|
match (&prerender, &search_term) {
|
||||||
left.iter_mut()
|
|
||||||
.rev()
|
|
||||||
.enumerate()
|
|
||||||
.map(move |(idx, p)| (start_point - (idx + 1), p))
|
|
||||||
)
|
|
||||||
.take(match (&prerender, &search_term) {
|
|
||||||
// If the user has limited the amount of pages they want to prerender, then we
|
// If the user has limited the amount of pages they want to prerender, then we
|
||||||
// just do what they ask. Nice and easy.
|
// just do what they ask. Nice and easy.
|
||||||
(PrerenderLimit::Limited(l), _) => l.get(),
|
(PrerenderLimit::Limited(l), _) => l.get(),
|
||||||
// If they haven't limited it, but we don't have any search term that we're
|
// If they haven't limited it, but we don't have any search term that we're
|
||||||
// currently looking for, just go for all of it
|
// currently looking for, just go for all of it
|
||||||
(PrerenderLimit::All, None) => n_pages,
|
(PrerenderLimit::All, None) => n_pages.get(),
|
||||||
// If they haven't limited it, and we DO have a search term we need to look
|
// If they haven't limited it, and we DO have a search term we need to look
|
||||||
// for, just do 20 so that we don't dramatically slow down the search process
|
// for, just do 20 so that we don't dramatically slow down the search process
|
||||||
// since they've specifically initiated that and so we want it to take priority
|
// since they've specifically initiated that and so we want it to take priority
|
||||||
@@ -243,15 +252,15 @@ pub fn start_rendering(
|
|||||||
if any_not_searched {
|
if any_not_searched {
|
||||||
20
|
20
|
||||||
} else {
|
} else {
|
||||||
n_pages
|
n_pages.get()
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
));
|
||||||
let area_w = f32::from(area.width) * f32::from(col_w);
|
|
||||||
let area_h = f32::from(area.height) * f32::from(col_h);
|
|
||||||
|
|
||||||
// we go through each page
|
// we go through each page
|
||||||
for (num, rendered) in page_iter {
|
for page_num in page_iter {
|
||||||
|
let rendered = &mut rendered[page_num];
|
||||||
|
|
||||||
// we only want to continue if one of the following is met:
|
// we only want to continue if one of the following is met:
|
||||||
// 1. It failed to render last time (we want to retry)
|
// 1. It failed to render last time (we want to retry)
|
||||||
// 2. The `contained_term` is set to Unknown, meaning that we need to at least
|
// 2. The `contained_term` is set to Unknown, meaning that we need to at least
|
||||||
@@ -260,18 +269,9 @@ pub fn start_rendering(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we've been told to change the area that we're rendering to,
|
|
||||||
// or if we're told to rerender
|
|
||||||
match receiver.try_recv() {
|
|
||||||
// If it's disconnected, then the main loop is done, so we should just give up
|
|
||||||
Err(TryRecvError::Disconnected) => return Ok(()),
|
|
||||||
Ok(notif) => handle_notif!(notif),
|
|
||||||
Err(TryRecvError::Empty) => ()
|
|
||||||
};
|
|
||||||
|
|
||||||
// We know this is in range 'cause we're iterating over it but we still just want
|
// We know this is in range 'cause we're iterating over it but we still just want
|
||||||
// to be safe
|
// to be safe
|
||||||
let page = match doc.load_page(num as i32) {
|
let page = match doc.load_page(page_num as i32) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
sender.send(Err(RenderError::Doc(e)))?;
|
sender.send(Err(RenderError::Doc(e)))?;
|
||||||
continue;
|
continue;
|
||||||
@@ -310,13 +310,22 @@ pub fn start_rendering(
|
|||||||
cell_w: (ctx.surface_w / f32::from(col_w)) as u16,
|
cell_w: (ctx.surface_w / f32::from(col_w)) as u16,
|
||||||
cell_h: (ctx.surface_h / f32::from(col_h)) as u16
|
cell_h: (ctx.surface_h / f32::from(col_h)) as u16
|
||||||
},
|
},
|
||||||
page_num: num,
|
page_num,
|
||||||
result_rects: ctx.result_rects
|
result_rects: ctx.result_rects
|
||||||
})))?;
|
})))?;
|
||||||
}
|
}
|
||||||
// And if we got an error, then obviously we need to propagate that
|
// And if we got an error, then obviously we need to propagate that
|
||||||
Err(e) => sender.send(Err(RenderError::Doc(e)))?
|
Err(e) => sender.send(Err(RenderError::Doc(e)))?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if we've been told to change the area that we're rendering to,
|
||||||
|
// or if we're told to rerender
|
||||||
|
match receiver.try_recv() {
|
||||||
|
// If it's disconnected, then the main loop is done, so we should just give up
|
||||||
|
Err(TryRecvError::Disconnected) => return Ok(()),
|
||||||
|
Ok(notif) => handle_notif!(notif),
|
||||||
|
Err(TryRecvError::Empty) => ()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now, if we have a search term, we want to look through the rest of the document past
|
// Now, if we have a search term, we want to look through the rest of the document past
|
||||||
@@ -371,7 +380,7 @@ pub fn start_rendering(
|
|||||||
|
|
||||||
// now, we want to check if we've gone past the end - if so, we go back to the
|
// now, we want to check if we've gone past the end - if so, we go back to the
|
||||||
// beginning so we can get the pages before the current one.
|
// beginning so we can get the pages before the current one.
|
||||||
if search_start > n_pages {
|
if search_start > n_pages.get() {
|
||||||
if start_point == 0 {
|
if start_point == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -544,3 +553,14 @@ fn count_search_results(page: &Page, search_term: &str) -> Result<usize, mupdf::
|
|||||||
Ok(count)
|
Ok(count)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PopOnNext<'a> {
|
||||||
|
inner: &'a mut VecDeque<usize>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for PopOnNext<'a> {
|
||||||
|
type Item = usize;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.inner.pop_front()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+106
@@ -1,3 +1,5 @@
|
|||||||
|
use std::num::NonZeroUsize;
|
||||||
|
|
||||||
use ratatui::widgets::Widget;
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
pub struct Skip {
|
pub struct Skip {
|
||||||
@@ -19,3 +21,107 @@ impl Widget for Skip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlusOrMinus {
|
||||||
|
Plus,
|
||||||
|
Minus
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct InterleavedAroundWithMax {
|
||||||
|
// starts at this number
|
||||||
|
around: usize,
|
||||||
|
inclusive_min: usize,
|
||||||
|
// this iterator can only produce values in [0..max)
|
||||||
|
exclusive_max: NonZeroUsize,
|
||||||
|
// the next time we call `next()`, this value should be combined with `around` according to
|
||||||
|
// `next_op`, then, after next_op is inverted, incremented if next_op was negative before being
|
||||||
|
// inverted.
|
||||||
|
next_change: usize,
|
||||||
|
// How `next_change` should be applied to `around` next time `next()` is called
|
||||||
|
next_op: PlusOrMinus
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterleavedAroundWithMax {
|
||||||
|
/// the following must hold or else this is liable to panic or produce nonsense values:
|
||||||
|
/// - inclusive_min < exclusive_max
|
||||||
|
/// - inclusive_min <= around <= exclusive_max
|
||||||
|
pub fn new(around: usize, inclusive_min: usize, exclusive_max: NonZeroUsize) -> Self {
|
||||||
|
Self {
|
||||||
|
around,
|
||||||
|
inclusive_min,
|
||||||
|
exclusive_max,
|
||||||
|
next_change: 0,
|
||||||
|
next_op: PlusOrMinus::Minus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for InterleavedAroundWithMax {
|
||||||
|
type Item = usize;
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let actual_change = self.next_change % (self.exclusive_max.get() - self.inclusive_min);
|
||||||
|
|
||||||
|
let to_return = match self.next_op {
|
||||||
|
// If we're supposed to add them and we need it to wrap, then try to add them together
|
||||||
|
// 'cause we need special behavior if it overflows usize's limits
|
||||||
|
PlusOrMinus::Plus => match self.around.checked_add(actual_change) {
|
||||||
|
// If we added it and it's within the range, we're chillin
|
||||||
|
Some(next_val) if next_val < self.exclusive_max.get() => next_val,
|
||||||
|
// If we added it and it's not within the range, do next_val % (self.max + 1), e.g.
|
||||||
|
// if max is 20, we were at 15, and we added 7, we should get 1 (because +5 would
|
||||||
|
// hit the max, then 0, then 1). So adding 1 before the modulo makes it hit the
|
||||||
|
// right numbers. And we can be sure the + here doesn't overflow 'cause we already
|
||||||
|
// checked the `usize::MAX` up above
|
||||||
|
Some(next_val) => (next_val % self.exclusive_max.get()) + self.inclusive_min,
|
||||||
|
// If we added them and it would've overflowed usize::MAX, then we see how much
|
||||||
|
// of the change would be remaining after reaching `max`
|
||||||
|
None =>
|
||||||
|
(actual_change - (self.exclusive_max.get() - actual_change))
|
||||||
|
+ self.inclusive_min,
|
||||||
|
},
|
||||||
|
PlusOrMinus::Minus => match self.around.checked_sub(actual_change) {
|
||||||
|
// If we can just minus it, cool cool. All is good.
|
||||||
|
Some(next_val) if next_val >= self.inclusive_min => next_val,
|
||||||
|
// If we can minus it but it goes below our min, then see how much below it went
|
||||||
|
// and just manually wrap it around
|
||||||
|
Some(next_val) => self.exclusive_max.get() - (self.inclusive_min - next_val),
|
||||||
|
// If we can't...
|
||||||
|
None => {
|
||||||
|
// then we see how much of the change would be remaining after hitting the
|
||||||
|
// minimum
|
||||||
|
let remaining = actual_change - (self.around - self.inclusive_min);
|
||||||
|
|
||||||
|
// and then we take that away from the top!
|
||||||
|
self.exclusive_max.get() - remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.next_op = match self.next_op {
|
||||||
|
PlusOrMinus::Plus => PlusOrMinus::Minus,
|
||||||
|
PlusOrMinus::Minus => {
|
||||||
|
self.next_change = (self.next_change + 1) % self.exclusive_max.get();
|
||||||
|
PlusOrMinus::Plus
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(to_return)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn iter_works() {
|
||||||
|
let got = InterleavedAroundWithMax::new(5, 2, NonZeroUsize::new(21).unwrap())
|
||||||
|
.take(30)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(got, vec![
|
||||||
|
5, 6, 4, 7, 3, 8, 2, 9, 20, 10, 19, 11, 18, 12, 17, 13, 16, 14, 15, 15, 14, 16, 13, 17,
|
||||||
|
12, 18, 11, 19, 10, 20
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ pub struct Tui {
|
|||||||
showing_help_msg: bool
|
showing_help_msg: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default)]
|
||||||
struct LastRender {
|
struct LastRender {
|
||||||
// Used as a way to track if we need to draw the images, to save ratatui from doing a lot of
|
// Used as a way to track if we need to draw the images, to save ratatui from doing a lot of
|
||||||
// diffing work
|
// diffing work
|
||||||
|
|||||||
Reference in New Issue
Block a user