diff --git a/Cargo.lock b/Cargo.lock index a1e161c..f400132 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,6 +285,30 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitcode" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648bd963d2e5d465377acecfb4b827f9f553b6bc97a8f61715779e9ed9e52b74" +dependencies = [ + "arrayvec", + "bitcode_derive", + "bytemuck", + "glam", + "serde", +] + +[[package]] +name = "bitcode_derive" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffebfc2d28a12b262c303cb3860ee77b91bd83b1f20f0bd2a9693008e2f55a9e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -835,13 +859,34 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -852,7 +897,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.1", ] @@ -957,6 +1002,12 @@ dependencies = [ "regex", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -1070,7 +1121,7 @@ dependencies = [ "core-foundation", "core-graphics", "core-text", - "dirs", + "dirs 6.0.0", "dwrote", "float-ord", "freetype-sys", @@ -1209,6 +1260,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glam" +version = "0.30.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e12d847aeb25f41be4c0ec9587d624e9cd631bc007a8fd7ce3f5851e064c6460" + [[package]] name = "glob" version = "0.3.3" @@ -2653,6 +2710,17 @@ dependencies = [ "bitflags 2.9.4", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -3020,11 +3088,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" name = "tdf-viewer" version = "0.4.3" dependencies = [ + "bitcode", "console-subscriber", "cpuprofiler", "criterion", "crossterm", "csscolorparser 0.7.2", + "dirs 5.0.1", "flexi_logger", "flume", "futures-util", @@ -3039,10 +3109,24 @@ dependencies = [ "ratatui", "ratatui-image", "rayon", + "tempfile", "tokio", "xflags", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "terminfo" version = "0.9.0" @@ -3866,6 +3950,15 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3902,6 +3995,21 @@ dependencies = [ "windows-link 0.2.0", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3944,6 +4052,12 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3956,6 +4070,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3968,6 +4088,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3992,6 +4118,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4004,6 +4136,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4016,6 +4154,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4028,6 +4172,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 86077a1..091730c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,8 @@ rayon = { version = "*", default-features = false } # kittage = { path = "../kittage/", features = ["crossterm-tokio", "image-crate", "log"] } kittage = { git = "https://github.com/itsjunetime/kittage.git", features = ["crossterm-tokio", "image-crate", "log"] } memmap2 = "*" +bitcode = "0.6" +dirs = "5.0" # logging log = "0.4.27" @@ -65,6 +67,7 @@ cbz = ["mupdf/cbz"] [dev-dependencies] criterion = { version = "0.7.0", features = ["async_tokio"] } cpuprofiler = "0.0.4" +tempfile = "3.0" [[bench]] name = "rendering" diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..58216ca --- /dev/null +++ b/src/history.rs @@ -0,0 +1,140 @@ +use std::{collections::HashMap, fs, path::PathBuf}; + +use bitcode::{Decode, Encode}; +use dirs::config_dir; + +use crate::WrappedErr; + +#[derive(Decode, Encode, Default)] +pub struct DocumentHistory { + pub last_pages_opened: HashMap +} + +impl DocumentHistory { + pub fn load() -> Result { + let path = Self::history_path()?; + let data = fs::read(path) + .map_err(|e| WrappedErr(format!("Failed to read history file: {e}").into()))?; + bitcode::decode(&data) + .map_err(|e| WrappedErr(format!("Failed to decode history file: {e}").into())) + } + + pub fn save(&self) -> Result<(), WrappedErr> { + let path = Self::history_path()?; + fs::write(path, bitcode::encode(self)) + .map_err(|e| WrappedErr(format!("Failed to write history file: {e}").into()))?; + Ok(()) + } + + fn history_path() -> Result { + config_dir() + .map(|p| p.join("tdf.history.bin")) + .ok_or_else(|| WrappedErr("Could not determine history directory".into())) + } +} + +#[cfg(test)] +mod tests { + use std::fs; + + use tempfile::tempdir; + + use super::*; + + #[test] + fn test_default_history() { + let history = DocumentHistory::default(); + assert!(history.last_pages_opened.is_empty()); + } + + #[test] + fn test_history_serialization() { + let mut history = DocumentHistory::default(); + history + .last_pages_opened + .insert("/path/to/file1.pdf".to_string(), 5); + history + .last_pages_opened + .insert("/path/to/file2.pdf".to_string(), 10); + + let encoded = bitcode::encode(&history); + let deserialized: DocumentHistory = bitcode::decode(&encoded).unwrap(); + + assert_eq!( + deserialized.last_pages_opened.get("/path/to/file1.pdf"), + Some(&5) + ); + assert_eq!( + deserialized.last_pages_opened.get("/path/to/file2.pdf"), + Some(&10) + ); + } + + #[test] + fn test_history_with_temp_dir() { + let temp_dir = tempdir().unwrap(); + let history_path = temp_dir.path().join("tdf.history.bin"); + + let mut history = DocumentHistory::default(); + history + .last_pages_opened + .insert("/test/file.pdf".to_string(), 42); + + let encoded = bitcode::encode(&history); + fs::write(&history_path, encoded).unwrap(); + + let data = fs::read(&history_path).unwrap(); + let loaded_history: DocumentHistory = bitcode::decode(&data).unwrap(); + + assert_eq!( + loaded_history.last_pages_opened.get("/test/file.pdf"), + Some(&42) + ); + } + + #[test] + fn test_load_with_invalid_binary() { + let temp_dir = tempdir().unwrap(); + let history_path = temp_dir.path().join("tdf.history.bin"); + + fs::write(&history_path, b"invalid binary data").unwrap(); + + let data = fs::read(&history_path).unwrap(); + let result: Result = bitcode::decode(&data); + assert!(result.is_err()); + } + + #[test] + fn test_history_with_empty_file() { + let temp_dir = tempdir().unwrap(); + let history_path = temp_dir.path().join("tdf.history.bin"); + + fs::write(&history_path, b"").unwrap(); + + let data = fs::read(&history_path).unwrap(); + let result: Result = bitcode::decode(&data); + assert!(result.is_err()); + } + + #[test] + fn test_history_save_and_load() { + let temp_dir = tempdir().unwrap(); + let test_history_path = temp_dir.path().join("tdf.history.bin"); + + let mut history = DocumentHistory::default(); + history + .last_pages_opened + .insert("/test/file.pdf".to_string(), 123); + + let encoded = bitcode::encode(&history); + fs::write(&test_history_path, encoded).unwrap(); + + let data = fs::read(&test_history_path).unwrap(); + let loaded_history: DocumentHistory = bitcode::decode(&data).unwrap(); + + assert_eq!( + loaded_history.last_pages_opened.get("/test/file.pdf"), + Some(&123) + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 08c7de4..8a1068d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,22 @@ -use std::num::NonZeroUsize; +use std::{borrow::Cow, num::NonZeroUsize}; #[global_allocator] static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; +pub struct WrappedErr(pub Cow<'static, str>); + +impl std::fmt::Display for WrappedErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Debug for WrappedErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + +impl std::error::Error for WrappedErr {} #[derive(PartialEq)] pub enum PrerenderLimit { @@ -10,6 +25,7 @@ pub enum PrerenderLimit { } pub mod converter; +pub mod history; pub mod kitty; pub mod renderer; pub mod skip; diff --git a/src/main.rs b/src/main.rs index 418dfbe..0c8d253 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,6 @@ use core::{ num::{NonZeroU32, NonZeroUsize} }; use std::{ - borrow::Cow, ffi::OsString, io::{BufReader, Read, Stdout, Write, stdout}, path::PathBuf @@ -32,30 +31,14 @@ use ratatui_image::{ picker::{Picker, ProtocolType} }; use tdf::{ - PrerenderLimit, + PrerenderLimit, WrappedErr, converter::{ConvertedPage, ConverterMsg, run_conversion_loop}, + history::DocumentHistory, kitty::{KittyDisplay, display_kitty_images, do_shms_work, run_action}, renderer::{self, RenderError, RenderInfo, RenderNotif}, tui::{BottomMessage, InputAction, MessageSetting, Tui} }; -// Dummy struct for easy errors in main -struct WrappedErr(Cow<'static, str>); - -impl std::fmt::Display for WrappedErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::fmt::Debug for WrappedErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(self, f) - } -} - -impl std::error::Error for WrappedErr {} - fn reset_term() { _ = execute!( std::io::stdout(), @@ -267,12 +250,20 @@ async fn inner_main() -> Result<(), WrappedErr> { || "Unknown file".into(), |n| n.to_string_lossy().to_string() ); - let tui = Tui::new( + let mut tui = Tui::new( file_name, flags.max_wide, flags.r_to_l.unwrap_or_default(), is_kitty ); + let mut document_history = DocumentHistory::load().unwrap_or_else(|e| { + WrappedErr(format!("Couldn't initialize document history: {e}").into()); + DocumentHistory::default() + }); + let restored_page = document_history + .last_pages_opened + .get(&path.to_string_lossy().to_string()) + .copied(); let backend = CrosstermBackend::new(std::io::stdout()); let mut term = Terminal::new(backend).map_err(|e| { @@ -313,6 +304,20 @@ async fn inner_main() -> Result<(), WrappedErr> { let tui_rx = tui_rx.into_stream(); let from_converter = from_converter.into_stream(); + if let Some(page) = restored_page { + tui.set_page(page); + to_renderer + .send(RenderNotif::JumpToPage(page)) + .map_err(|e| { + WrappedErr(format!("Couldn't tell renderer to jump to restored page: {e}").into()) + })?; + to_converter + .send(ConverterMsg::GoToPage(page)) + .map_err(|e| { + WrappedErr(format!("Couldn't tell converter to jump to restored page: {e}").into()) + })?; + } + enter_redraw_loop( ev_stream, to_renderer, @@ -320,7 +325,7 @@ async fn inner_main() -> Result<(), WrappedErr> { to_converter, from_converter, fullscreen, - tui, + &mut tui, &mut term, main_area, font_size @@ -346,6 +351,14 @@ async fn inner_main() -> Result<(), WrappedErr> { drop(maybe_logger); + document_history + .last_pages_opened + .insert(path.to_string_lossy().to_string(), tui.page); + + if let Err(e) = document_history.save() { + WrappedErr(format!("Failed to save last opened page: {e}").into()); + } + Ok(()) } @@ -358,7 +371,7 @@ async fn enter_redraw_loop( to_converter: Sender, mut from_converter: RecvStream<'_, Result>, mut fullscreen: bool, - mut tui: Tui, + tui: &mut Tui, term: &mut Terminal>, mut main_area: tdf::tui::RenderLayout, font_size: FontSize diff --git a/src/tui.rs b/src/tui.rs index b08f950..d55b6f6 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -805,7 +805,7 @@ impl Tui { }))); } - fn set_page(&mut self, page: usize) { + pub fn set_page(&mut self, page: usize) { if page != self.page { // mark that we need to re-render the images self.last_render.rect = Rect::default();