diff --git a/Cargo.lock b/Cargo.lock index 9b39537..6b6a5f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,12 +1313,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1600,10 +1599,10 @@ dependencies = [ [[package]] name = "kittage" -version = "0.1.0" -source = "git+https://github.com/itsjunetime/kittage.git#0ec72474513721f9960332067fd740afab3ed255" +version = "0.1.1" +source = "git+https://github.com/itsjunetime/kittage.git#fbd2a7d42296771e044575c009ee34616a3964e5" dependencies = [ - "base64 0.22.1", + "base64-simd", "crossterm", "futures-core", "image", @@ -2427,9 +2426,9 @@ dependencies = [ [[package]] name = "psx-shm" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d6abd69b7d14272c59b72b14e3ff058232197ac0846480d58f047140ce1a2" +checksum = "b9d9d53299b024cf527e0cdc4e871cee6e7aa28955c1214b78799804102a2e3d" dependencies = [ "memmap2", "rustix", @@ -3944,18 +3943,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.28" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.28" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 1f4ace3..3240db2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,10 +39,10 @@ xflags = "0.4.0-pre.2" mimalloc = "0.1.43" nix = { version = "0.30.0", features = ["signal"] } mupdf = { git = "https://github.com/messense/mupdf-rs.git", rev = "2e0fae910fac8048c7008211fc4d3b9f5d227a07", default-features = false, features = ["svg", "system-fonts", "img"] } -rayon = { version = "*", default-features = false } +rayon = { version = "1", 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 = "*" +memmap2 = "0" csscolorparser = { version = "0.8.0", default-features = false } # logging @@ -75,90 +75,205 @@ name = "for_profiling" path = "./benches/for_profiling.rs" [lints.clippy] -uninlined_format_args = "warn" -redundant_closure_for_method_calls = "warn" -cast_lossless = "warn" -single_char_pattern = "warn" -manual_let_else = "warn" -ignored_unit_patterns = "warn" -range_plus_one = "warn" -unreadable_literal = "warn" -redundant_else = "warn" +alloc_instead_of_core = "warn" +allow_attributes = "warn" +as_pointer_underscore = "warn" +as_ptr_cast_mut = "warn" +as_underscore = "warn" assigning_clones = "warn" +assertions_on_result_states = "warn" bool_to_int_with_if = "warn" borrow_as_ptr = "warn" +branches_sharing_code = "warn" +cargo_common_metadata = "warn" +case_sensitive_file_extension_comparisons = "warn" +cast_lossless = "warn" cast_ptr_alignment = "warn" +cfg_not_test = "warn" checked_conversions = "warn" +clear_with_drain = "warn" +cloned_instead_of_copied = "warn" +coerce_container_to_any = "warn" +comparison_chain = "warn" copy_iterator = "warn" +create_dir = "warn" +debug_assert_with_mut_call = "warn" +decimal_literal_representation = "warn" default_trait_access = "warn" +deref_by_slicing = "warn" +doc_broken_link = "warn" +doc_link_code = "warn" doc_link_with_quotes = "warn" +elidable_lifetime_names = "warn" +empty_drop = "warn" empty_enums = "warn" +empty_enum_variants_with_brackets = "warn" +empty_structs_with_brackets = "warn" +equatable_if_let = "warn" +error_impl_error = "warn" +expl_impl_clone_on_copy = "warn" +explicit_deref_methods = "warn" explicit_into_iter_loop = "warn" explicit_iter_loop = "warn" +fallible_impl_from = "warn" +filetype_is_file = "warn" filter_map_next = "warn" flat_map_option = "warn" +fn_to_numeric_cast_any = "warn" fn_params_excessive_bools = "warn" -from_iter_instead_of_collect = "warn" +format_collect = "warn" +format_push_string = "warn" +get_unwrap = "warn" +if_then_some_else_none = "warn" +ignore_without_reason = "warn" +ignored_unit_patterns = "warn" implicit_clone = "warn" +imprecise_flops = "warn" index_refutable_slice = "warn" -inefficient_to_string = "warn" +indexing_slicing = "allow" # can't warn on this cause we basically have to do indexing for some ratatui apis +infinite_loop = "warn" invalid_upcast_comparisons = "warn" +ip_constant = "warn" iter_filter_is_ok = "warn" iter_filter_is_some = "warn" iter_not_returning_iterator = "warn" +iter_on_empty_collections = "warn" +iter_on_single_items = "warn" +large_digit_groups = "warn" large_futures = "warn" -large_stack_arrays = "warn" +large_include_file = "warn" +large_stack_frames = "warn" large_types_passed_by_value = "warn" linkedlist = "warn" +literal_string_with_formatting_args = "warn" +lossy_float_literal = "warn" macro_use_imports = "warn" manual_assert = "warn" manual_instant_elapsed = "warn" manual_is_power_of_two = "warn" manual_is_variant_and = "warn" +manual_let_else = "warn" +manual_midpoint = "warn" manual_ok_or = "warn" -manual_string_new = "warn" many_single_char_names = "warn" -manual_unwrap_or = "warn" +map_err_ignore = "warn" +map_unwrap_or = "warn" +map_with_unused_argument_over_ranges = "warn" match_same_arms = "warn" +match_wild_err_arm = "warn" match_wildcard_for_single_variants = "warn" maybe_infinite_iter = "warn" +mem_forget = "warn" mismatching_type_param_order = "warn" +missing_assert_message = "warn" missing_fields_in_debug = "warn" +mixed_read_write_in_expression = "warn" +multiple_unsafe_ops_per_block = "warn" +must_use_candidate = "warn" mut_mut = "warn" +mutex_atomic = "warn" +mutex_integer = "warn" +naive_bytecount = "warn" needless_bitwise_bool = "warn" +needless_collect = "warn" needless_continue = "warn" needless_for_each = "warn" +needless_pass_by_ref_mut = "warn" needless_pass_by_value = "warn" needless_raw_string_hashes = "warn" +needless_raw_strings = "warn" +negative_feature_names = "warn" no_effect_underscore_binding = "warn" no_mangle_with_rust_abi = "warn" +non_send_fields_in_send_ty = "warn" +non_std_lazy_statics = "warn" +non_zero_suggestions = "warn" +nonstandard_macro_braces = "warn" option_as_ref_cloned = "warn" option_option = "warn" +or_fun_call = "warn" +path_buf_push_overwrite = "warn" +pathbuf_init_then_push = "warn" +precedence_bits = "warn" ptr_as_ptr = "warn" ptr_cast_constness = "warn" +pub_underscore_fields = "warn" +pub_without_shorthand = "warn" range_minus_one = "warn" +range_plus_one = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +read_zero_byte_vec = "warn" +redundant_clone = "warn" +redundant_closure_for_method_calls = "warn" +redundant_else = "warn" +redundant_pub_crate = "warn" +redundant_test_prefix = "warn" ref_as_ptr = "warn" ref_binding_to_reference = "warn" ref_option = "warn" ref_option_ref = "warn" +rest_pat_in_fully_bound_structs = "warn" return_self_not_must_use = "warn" same_functions_in_if_condition = "warn" +self_named_module_files = "warn" +semicolon_if_nothing_returned = "warn" +semicolon_inside_block = "warn" should_panic_without_expect = "warn" -similar_names = "warn" +significant_drop_in_scrutinee = "warn" # I thought this was fixed in the 2024 edition. watever +significant_drop_tightening = "warn" +single_char_pattern = "warn" +single_option_map = "warn" stable_sort_primitive = "warn" str_split_at_newline = "warn" +string_lit_as_bytes = "warn" +string_lit_chars_any = "warn" +string_slice = "warn" struct_excessive_bools = "warn" struct_field_names = "warn" +suboptimal_flops = "warn" +suspicious_operation_groupings = "warn" +suspicious_xor_used_as_pow = "warn" +tests_outside_test_module = "warn" +trait_duplication_in_bounds = "warn" transmute_ptr_to_ptr = "warn" +trivial_regex = "warn" trivially_copy_pass_by_ref = "warn" +try_err = "warn" +tuple_array_conversions = "warn" +type_repetition_in_bounds = "warn" +unchecked_time_subtraction = "warn" +undocumented_unsafe_blocks = "warn" unicode_not_nfc = "warn" +uninhabited_references = "warn" +uninlined_format_args = "warn" unnecessary_box_returns = "warn" +unnecessary_debug_formatting = "warn" unnecessary_join = "warn" unnecessary_literal_bound = "warn" +unnecessary_safety_comment = "warn" +unnecessary_safety_doc = "warn" +unnecessary_self_imports = "warn" +unnecessary_semicolon = "warn" +unnecessary_struct_initialization = "warn" unnecessary_wraps = "warn" unnested_or_patterns = "warn" +unreadable_literal = "warn" +unsafe_derive_deserialize = "warn" unused_async = "warn" +unused_peekable = "warn" +unused_result_ok = "warn" +unused_rounding = "warn" unused_self = "warn" +unused_trait_names = "warn" +use_self = "warn" used_underscore_binding = "warn" used_underscore_items = "warn" +useless_let_if_seq = "warn" +verbose_bit_mask = "warn" +verbose_file_reads = "warn" +volatile_composites = "warn" +while_float = "warn" +wildcard_dependencies = "warn" +wildcard_imports = "warn" zero_sized_map_values = "warn" diff --git a/benches/for_profiling.rs b/benches/for_profiling.rs index ce32489..4bf2dd9 100644 --- a/benches/for_profiling.rs +++ b/benches/for_profiling.rs @@ -1,3 +1,5 @@ +use ratatui_image::picker::ProtocolType; + mod utils; const BLACK: i32 = 0; @@ -12,5 +14,5 @@ async fn main() { .nth(1) .expect("Please enter a file to profile"); - utils::render_doc(file, None, BLACK, WHITE).await; + utils::render_doc(file, None, BLACK, WHITE, ProtocolType::Kitty).await; } diff --git a/benches/rendering.rs b/benches/rendering.rs index fb70564..bbdc938 100644 --- a/benches/rendering.rs +++ b/benches/rendering.rs @@ -1,17 +1,15 @@ mod utils; -use std::{ - hint::black_box, - path::Path, - time::{SystemTime, UNIX_EPOCH} -}; +use std::{hint::black_box, path::Path}; -use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main, profiler::Profiler}; -use futures_util::StreamExt; +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use futures_util::StreamExt as _; +use ratatui_image::picker::ProtocolType; use tdf::{ converter::{ConvertedPage, ConverterMsg}, renderer::{PageInfo, RenderInfo, fill_default} }; +use tokio::runtime::Runtime; use utils::{ RenderState, handle_converter_msg, handle_renderer_msg, render_doc, start_all_rendering, start_converting_loop, start_rendering_loop @@ -23,90 +21,103 @@ const FILES: [&str; 3] = [ "benches/geotopo.pdf" ]; +const PROTOS: [ProtocolType; 3] = [ + ProtocolType::Kitty, + ProtocolType::Sixel, + ProtocolType::Iterm2 +]; + const BLACK: i32 = 0; const WHITE: i32 = i32::from_be_bytes([0, 0xff, 0xff, 0xff]); -fn render_full(c: &mut Criterion) { - for file in FILES { - c.bench_with_input(BenchmarkId::new("render_full", file), &file, |b, &file| { - b.to_async(tokio::runtime::Runtime::new().unwrap()) - .iter(|| render_doc(file, None, BLACK, WHITE)) - }); +fn for_all_combos( + name: &'static str, + mut f: impl FnMut(&Runtime, BenchmarkId, &'static str, ProtocolType) +) { + let rt = tokio::runtime::Runtime::new().unwrap(); + for proto in PROTOS { + for file in FILES { + f( + &rt, + BenchmarkId::new(name, format!("{file},{proto:?}")), + file, + proto + ); + } } } +fn render_full(c: &mut Criterion) { + for_all_combos("render_full", |rt, id, file, proto| { + _ = c.bench_with_input(id, &file, |b, &file| { + b.to_async(rt) + .iter(|| render_doc(file, None, BLACK, WHITE, proto)); + }); + }); +} + fn render_to_first_page(c: &mut Criterion) { - for file in FILES { - c.bench_with_input( - BenchmarkId::new("render_first_page", file), - &file, - |b, &file| { - b.to_async(tokio::runtime::Runtime::new().unwrap()) - .iter(|| render_first_page(file, BLACK, WHITE)) - } - ); - } + for_all_combos("render_first_page", |rt, id, file, proto| { + c.bench_with_input(id, &file, |b, &file| { + b.to_async(rt) + .iter(|| render_first_page(file, BLACK, WHITE, proto)); + }); + }); } fn only_converting(c: &mut Criterion) { - for file in FILES { - let runtime = tokio::runtime::Runtime::new().unwrap(); - let all_rendered = runtime.block_on(render_all_files(file, BLACK, WHITE)); + for_all_combos("only_converting", |rt, id, file, proto| { + let all_rendered = rt.block_on(render_all_files(file, BLACK, WHITE)); - c.bench_with_input( - BenchmarkId::new("only_converting", file), - &(all_rendered, file), - |b, (rendered, _)| { - b.to_async(tokio::runtime::Runtime::new().unwrap()) - .iter_with_setup(|| rendered.clone(), convert_all_files) - } - ); - } + c.bench_with_input(id, &all_rendered, |b, rendered| { + b.to_async(rt) + .iter_with_setup(|| rendered.clone(), |f| convert_all_files(f, proto)); + }); + }); } +/* fn search_short_common(c: &mut Criterion) { - for file in FILES { - c.bench_with_input( - BenchmarkId::new("search_short_common", file), - &file, - |b, &file| { - b.to_async(tokio::runtime::Runtime::new().unwrap()) - .iter(|| render_doc(file, Some("an"), BLACK, WHITE)) - } - ); - } + for_all_combos("search_short_common", |rt, id, file, proto| { + c.bench_with_input(id, &file, |b, &file| { + b.to_async(rt) + .iter(|| render_doc(file, Some("an"), BLACK, WHITE, proto)) + }); + }); } fn search_long_rare(c: &mut Criterion) { - for file in FILES { - c.bench_with_input( - BenchmarkId::new("search_long_rare", file), - &file, - |b, &file| { - b.to_async(tokio::runtime::Runtime::new().unwrap()) - .iter(|| render_doc(file, Some("this is long and rare"), BLACK, WHITE)) - } - ); - } + for_all_combos("search_long_rare", |rt, id, file, proto| { + c.bench_with_input(id, &file, |b, &file| { + b.to_async(rt) + .iter(|| render_doc(file, Some("this is long and rare"), BLACK, WHITE, proto)) + }); + }); } +*/ -pub async fn render_first_page(path: impl AsRef, black: i32, white: i32) { +pub async fn render_first_page( + path: impl AsRef, + black: i32, + white: i32, + proto: ProtocolType +) { let RenderState { mut from_render_rx, mut from_converter_rx, mut pages, - mut to_converter_tx, + to_converter_tx, to_render_tx - } = start_all_rendering(path, black, white); + } = start_all_rendering(path, black, white, proto); // we only want to render until the first page is ready to be printed while pages.iter().all(Option::is_none) { tokio::select! { Some(renderer_msg) = from_render_rx.next() => { - handle_renderer_msg(renderer_msg, &mut pages, &mut to_converter_tx); + handle_renderer_msg(renderer_msg, &mut pages, &to_converter_tx); }, Some(converter_msg) = from_converter_rx.next() => { - handle_converter_msg(converter_msg, &mut pages, &mut to_converter_tx); + handle_converter_msg(converter_msg, &mut pages, &to_converter_tx); } } } @@ -129,7 +140,7 @@ async fn render_all_files(path: &'static str, black: i32, white: i32) -> Vec Vec) { +async fn convert_all_files(files: Vec, proto: ProtocolType) { let num_files = files.len(); - let (mut from_converter_rx, to_converter_tx) = start_converting_loop(num_files); + let (mut from_converter_rx, to_converter_tx) = start_converting_loop(proto, num_files); to_converter_tx .send(ConverterMsg::NumPages(num_files)) @@ -181,10 +192,12 @@ async fn convert_all_files(files: Vec) { black_box(converted); } +/* struct CpuProfiler; -impl Profiler for CpuProfiler { +impl criterion::profiler::Profiler for CpuProfiler { fn start_profiling(&mut self, benchmark_id: &str, _: &std::path::Path) { + use std::time::{SystemTime, UNIX_EPOCH} let file = format!( "./{}-{}.profile", benchmark_id.replace('/', "-"), @@ -200,10 +213,13 @@ impl Profiler for CpuProfiler { cpuprofiler::PROFILER.lock().unwrap().stop().unwrap(); } } +*/ criterion_group!( name = benches; - config = Criterion::default().sample_size(40).with_profiler(CpuProfiler); - targets = render_full, render_to_first_page, only_converting, search_short_common, search_long_rare + // config = Criterion::default().sample_size(40).with_profiler(CpuProfiler); + config = Criterion::default().sample_size(40); + // targets = render_full, render_to_first_page, only_converting, search_short_common, search_long_rare + targets = render_full, render_to_first_page, only_converting ); criterion_main!(benches); diff --git a/benches/utils.rs b/benches/utils.rs index e3785bd..3b7443d 100644 --- a/benches/utils.rs +++ b/benches/utils.rs @@ -13,7 +13,7 @@ use tdf::{ pub fn handle_renderer_msg( msg: Result, pages: &mut Vec>, - to_converter_tx: &mut Sender + to_converter_tx: &Sender ) { match msg { Ok(RenderInfo::NumPages(num)) => { @@ -30,7 +30,7 @@ pub fn handle_renderer_msg( pub fn handle_converter_msg( msg: Result, pages: &mut [Option], - to_converter_tx: &mut Sender + to_converter_tx: &Sender ) { let page = msg.expect("Got error from converter"); let num = page.num; @@ -106,7 +106,9 @@ pub fn start_rendering_loop( (from_render_rx, to_render_tx) } +#[must_use] pub fn start_converting_loop( + proto: ProtocolType, prerender: usize ) -> ( RecvStream<'static, Result>, @@ -116,7 +118,7 @@ pub fn start_converting_loop( let (to_main_tx, from_converter_rx) = unbounded(); let mut picker = Picker::from_fontsize(FONT_SIZE); - picker.set_protocol_type(ProtocolType::Kitty); + picker.set_protocol_type(proto); tokio::spawn(run_conversion_loop( to_main_tx, @@ -131,9 +133,14 @@ pub fn start_converting_loop( (from_converter_rx, to_converter_tx) } -pub fn start_all_rendering(path: impl AsRef, black: i32, white: i32) -> RenderState { +pub fn start_all_rendering( + path: impl AsRef, + black: i32, + white: i32, + proto: ProtocolType +) -> RenderState { let (from_render_rx, to_render_tx) = start_rendering_loop(path, black, white); - let (from_converter_rx, to_converter_tx) = start_converting_loop(20); + let (from_converter_rx, to_converter_tx) = start_converting_loop(proto, 20); let pages: Vec> = Vec::new(); @@ -146,14 +153,20 @@ pub fn start_all_rendering(path: impl AsRef, black: i32, white: i32) -> Re } } -pub async fn render_doc(path: impl AsRef, search_term: Option<&str>, black: i32, white: i32) { +pub async fn render_doc( + path: impl AsRef, + search_term: Option<&str>, + black: i32, + white: i32, + proto: ProtocolType +) { let RenderState { mut from_render_rx, mut from_converter_rx, mut pages, - mut to_converter_tx, + to_converter_tx, to_render_tx - } = start_all_rendering(path, black, white); + } = start_all_rendering(path, black, white, proto); if let Some(term) = search_term { to_render_tx @@ -164,10 +177,10 @@ pub async fn render_doc(path: impl AsRef, search_term: Option<&str>, black while pages.is_empty() || pages.iter().any(Option::is_none) { tokio::select! { Some(renderer_msg) = from_render_rx.next() => { - handle_renderer_msg(renderer_msg, &mut pages, &mut to_converter_tx); + handle_renderer_msg(renderer_msg, &mut pages, &to_converter_tx); }, Some(converter_msg) = from_converter_rx.next() => { - handle_converter_msg(converter_msg, &mut pages, &mut to_converter_tx); + handle_converter_msg(converter_msg, &mut pages, &to_converter_tx); } } } diff --git a/src/converter.rs b/src/converter.rs index a072b58..ea07a86 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -1,19 +1,19 @@ use std::{ - num::{NonZeroU32, NonZeroUsize}, + num::NonZeroUsize, time::{SystemTime, UNIX_EPOCH} }; use flume::{Receiver, SendError, Sender, TryRecvError}; -use futures_util::stream::StreamExt; +use futures_util::stream::StreamExt as _; use image::DynamicImage; -use kittage::NumberOrId; +use kittage::{NumberOrId, action::NONZERO_ONE}; use ratatui::layout::Rect; use ratatui_image::{ Resize, picker::{Picker, ProtocolType}, protocol::Protocol }; -use rayon::iter::ParallelIterator; +use rayon::iter::ParallelIterator as _; use crate::{ renderer::{PageInfo, RenderError, fill_default}, @@ -37,6 +37,7 @@ pub enum ConvertedImage { } impl ConvertedImage { + #[must_use] pub fn w_h(&self) -> (u16, u16) { match self { Self::Generic(prot) => { @@ -67,7 +68,7 @@ pub enum ConverterMsg { pub async fn run_conversion_loop( sender: Sender>, receiver: Receiver, - mut picker: Picker, + picker: Picker, prerender: usize, shms_work: bool ) -> Result<(), SendError>> { @@ -77,7 +78,7 @@ pub async fn run_conversion_loop( fn next_page( images: &mut [Option], - picker: &mut Picker, + picker: &Picker, page: usize, iteration: &mut usize, prerender: usize, @@ -126,7 +127,7 @@ pub async fn run_conversion_loop( .for_each(|(_, _, px)| px.0[2] = px.0[2].saturating_sub(u8::MAX / 2)); }, _ => unreachable!() - }; + } let img_area = Rect { width: page_info.img_data.cell_w, @@ -140,18 +141,20 @@ pub async fn run_conversion_loop( let rn = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() - .as_millis() % 1_000_000; + .as_nanos() % 1_000_000; let mut img = if shms_work { - kittage::image::Image::shm_from(dyn_img, &format!("tdf_{pid}_{rn}_{page_num}")) + kittage::image::Image::shm_from(dyn_img, &format!("/tdf_{pid}_{rn}_{page_num}")) .map_err(|e| { - RenderError::Converting(format!("Couldn't write to shm: {e}")) + RenderError::Converting(format!("Couldn't write to shm: {e:?}")) })? } else { kittage::image::Image::from(dyn_img) }; - img.num_or_id = NumberOrId::Id(NonZeroU32::new(page_num as u32 + 1).unwrap()); + // if ur pdf has 4 billion pages then you deserve to suffer + img.num_or_id = NumberOrId::Id(NONZERO_ONE.saturating_add(page_num as u32)); + ConvertedImage::Kitty { img: MaybeTransferred::NotYet(img), cell_w: page_info.img_data.cell_w, @@ -214,7 +217,7 @@ pub async fn run_conversion_loop( match next_page( &mut images, - &mut picker, + &picker, page, &mut iteration, prerender, diff --git a/src/kitty.rs b/src/kitty.rs index 97b4184..f97842b 100644 --- a/src/kitty.rs +++ b/src/kitty.rs @@ -60,8 +60,8 @@ impl Write for DbgWriter { } } -pub async fn run_action<'image, 'data, 'es>( - action: Action<'image, 'data>, +pub async fn run_action<'es>( + action: Action<'_, '_>, ev_stream: &'es mut EventStream ) -> Result::Error>> { let writer = DbgWriter { diff --git a/src/lib.rs b/src/lib.rs index 08c7de4..806eb63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ pub struct ScaledResult { scale_factor: f32 } +#[must_use] pub fn scale_img_for_area( (img_width, img_height): (f32, f32), (area_width, area_height): (f32, f32), diff --git a/src/main.rs b/src/main.rs index ae1bbbe..d4013e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use core::{ use std::{ borrow::Cow, ffi::OsString, - io::{BufReader, Read, Stdout, Write, stdout}, + io::{BufReader, Read as _, Stdout, Write as _, stdout}, path::PathBuf }; @@ -19,13 +19,13 @@ use crossterm::{ }; use flexi_logger::FileSpec; use flume::{Sender, r#async::RecvStream}; -use futures_util::{FutureExt, stream::StreamExt}; +use futures_util::{FutureExt as _, stream::StreamExt as _}; use kittage::{ action::Action, delete::{ClearOrDelete, DeleteConfig, WhichToDelete}, error::{TerminalError, TransmitError} }; -use notify::{Event, EventKind, RecursiveMode, Watcher}; +use notify::{Event, EventKind, RecursiveMode, Watcher as _}; use ratatui::{Terminal, backend::CrosstermBackend}; use ratatui_image::{ FontSize, @@ -57,23 +57,25 @@ impl std::fmt::Debug for WrappedErr { impl std::error::Error for WrappedErr {} fn reset_term() { + _ = disable_raw_mode(); _ = execute!( std::io::stdout(), LeaveAlternateScreen, crossterm::cursor::Show, crossterm::event::DisableMouseCapture - ) + ); } #[tokio::main] async fn main() -> Result<(), WrappedErr> { - inner_main().await.inspect_err(|_| reset_term()) + let result = inner_main().await; + reset_term(); + result } async fn inner_main() -> Result<(), WrappedErr> { let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - _ = disable_raw_mode(); reset_term(); hook(info); })); @@ -135,10 +137,8 @@ async fn inner_main() -> Result<(), WrappedErr> { // need to keep it around throughout the lifetime of the program, but don't rly need to use it. // Just need to make sure it doesn't get dropped yet. - let mut maybe_logger = None; - - if std::env::var("RUST_LOG").is_ok() { - maybe_logger = Some( + let maybe_logger = if std::env::var("RUST_LOG").is_ok() { + Some( flexi_logger::Logger::try_with_env() .map_err(|e| WrappedErr(format!("Couldn't create initial logger: {e}").into()))? .log_to_file(FileSpec::try_from("./debug.log").map_err(|e| { @@ -146,8 +146,10 @@ async fn inner_main() -> Result<(), WrappedErr> { })?) .start() .map_err(|e| WrappedErr(format!("Can't start logger: {e}").into()))? - ); - } + ) + } else { + None + }; let (watch_to_render_tx, render_rx) = flume::unbounded(); let to_renderer = watch_to_render_tx.clone(); @@ -159,7 +161,7 @@ async fn inner_main() -> Result<(), WrappedErr> { watch_to_tui_tx, watch_to_render_tx, path.file_name() - .ok_or(WrappedErr("Path does not have a last component??".into()))? + .ok_or_else(|| WrappedErr("Path does not have a last component??".into()))? .to_owned() )) .map_err(|e| WrappedErr(format!("Couldn't start watching the provided file: {e}").into()))?; @@ -331,17 +333,7 @@ async fn inner_main() -> Result<(), WrappedErr> { ) })?; - execute!( - term.backend_mut(), - LeaveAlternateScreen, - crossterm::cursor::Show, - crossterm::event::DisableMouseCapture - ) - .unwrap(); - disable_raw_mode().unwrap(); - drop(maybe_logger); - Ok(()) } @@ -550,18 +542,18 @@ fn get_font_size_through_stdio() -> Result<(u16, u16), WrappedErr> { )); }; - let h = h.parse::().map_err(|_| { + let h = h.parse::().map_err(|e| { WrappedErr( format!( - "Your terminal said its height is {h}, but that is not a 16-bit unsigned integer" + "Your terminal said its height is {h}, but that is not a 16-bit unsigned integer: {e}" ) .into() ) })?; - let w = w.parse::().map_err(|_| { + let w = w.parse::().map_err(|e| { WrappedErr( format!( - "Your terminal said its width is {w}, but that is not a 16-bit unsigned integer" + "Your terminal said its width is {w}, but that is not a 16-bit unsigned integer: {e}" ) .into() ) diff --git a/src/renderer.rs b/src/renderer.rs index 92d0164..879132d 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -78,7 +78,7 @@ pub fn fill_default(vec: &mut Vec, size: usize) { // 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 // probably be more performant if accessed by-value instead of through a reference. Probably. -#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)] +#[expect(clippy::needless_pass_by_value, clippy::too_many_arguments)] pub fn start_rendering( path: &str, sender: Sender>, @@ -116,7 +116,7 @@ pub fn start_rendering( // temporarily removed to facilitate a save or something like that) while let Ok(msg) = receiver.recv() { // and once that comes, just try to reload again - if let RenderNotif::Reload = msg { + if matches!(msg, RenderNotif::Reload) { continue 'reload; } } @@ -313,7 +313,7 @@ pub fn start_rendering( if let Err(e) = ctx.pixmap.write_to(&mut pixels, mupdf::ImageFormat::PNM) { sender.send(Err(RenderError::Doc(e)))?; continue; - }; + } log::debug!("got pixmap for page {page_num} with WxH {w}x{h}"); @@ -341,7 +341,7 @@ pub fn start_rendering( 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 @@ -434,7 +434,7 @@ pub fn start_rendering( return Ok(()); }; - handle_notif!(msg) + handle_notif!(msg); } } } @@ -565,7 +565,7 @@ struct PopOnNext<'a> { inner: &'a mut VecDeque } -impl<'a> Iterator for PopOnNext<'a> { +impl Iterator for PopOnNext<'_> { type Item = usize; fn next(&mut self) -> Option { self.inner.pop_front() diff --git a/src/skip.rs b/src/skip.rs index 9ac6c7a..b7db021 100644 --- a/src/skip.rs +++ b/src/skip.rs @@ -7,6 +7,7 @@ pub struct Skip { } impl Skip { + #[must_use] pub fn new(skip: bool) -> Self { Self { skip } } @@ -45,6 +46,7 @@ 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 + #[must_use] pub fn new(around: usize, inclusive_min: usize, exclusive_max: NonZeroUsize) -> Self { Self { around, diff --git a/src/tui.rs b/src/tui.rs index b5283ca..0145a39 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -109,7 +109,8 @@ pub struct RenderLayout { } impl Tui { - pub fn new(name: String, max_wide: Option, r_to_l: bool, is_kitty: bool) -> Tui { + #[must_use] + pub fn new(name: String, max_wide: Option, r_to_l: bool, is_kitty: bool) -> Self { Self { name, page: 0, @@ -124,6 +125,7 @@ impl Tui { } } + #[must_use] pub fn main_layout(frame: &Frame<'_>, fullscreened: bool) -> RenderLayout { if fullscreened { RenderLayout { @@ -276,7 +278,7 @@ impl Tui { } }]); } - }; + } // here we calculate how many pages can fit in the available area. let mut test_area_w = img_area.width; @@ -383,12 +385,13 @@ impl Tui { } fn render_loading_in(frame: &mut Frame<'_>, area: Rect) { - let loading_str = "Loading..."; - let inner_space = Layout::horizontal([Constraint::Length(loading_str.len() as u16)]) - .flex(Flex::Center) - .split(area); + const LOADING_STR: &str = "Loading..."; + let inner_space = + Layout::horizontal([Constraint::Length(const { LOADING_STR.len() as u16 })]) + .flex(Flex::Center) + .split(area); - let loading_span = Span::styled(loading_str, Style::new().fg(Color::Cyan)); + let loading_span = Span::styled(LOADING_STR, Style::new().fg(Color::Cyan)); frame.render_widget(loading_span, inner_space[0]); } @@ -417,6 +420,8 @@ impl Tui { PageChange::Prev => self.set_page(self.page.saturating_sub(diff)) } + // Yes these conversions could wrap around if you have > isize::MAX pages, but we already + // decided that you deserve to suffer if you have more than u32::MAX pages, so that's fine. match self.page as isize - old as isize { 0 => None, _ => Some(InputAction::JumpingToPage(self.page)) @@ -550,17 +555,11 @@ impl Tui { } pub fn handle_event(&mut self, ev: &Event) -> Option { - fn jump_to_page( - page: &mut usize, - rect: &mut Rect, - new_page: Option - ) -> Option { - new_page.map(|new_page| { - *page = new_page; - // Make sure we re-render - *rect = Rect::default(); - InputAction::JumpingToPage(new_page) - }) + fn jump_to_page(page: &mut usize, rect: &mut Rect, new_page: usize) -> InputAction { + *page = new_page; + // Make sure we re-render + *rect = Rect::default(); + InputAction::JumpingToPage(new_page) } match ev { @@ -617,30 +616,38 @@ impl Tui { 'n' if self.page < self.rendered.len() - 1 => { // TODO: If we can't find one, then maybe like block until we've verified // all the pages have been checked? - let next_page = self.rendered[(self.page + 1)..] + self.rendered[(self.page + 1)..] .iter() .enumerate() .find_map(|(idx, p)| { p.num_results .is_some_and(|num| num > 0) .then_some(self.page + 1 + idx) - }); - - jump_to_page(&mut self.page, &mut self.last_render.rect, next_page) - } - 'N' if self.page > 0 => { - let prev_page = self.rendered[..(self.page)] - .iter() - .rev() - .enumerate() - .find_map(|(idx, p)| { - p.num_results - .is_some_and(|num| num > 0) - .then_some(self.page - (idx + 1)) - }); - - jump_to_page(&mut self.page, &mut self.last_render.rect, prev_page) + }) + .map(|next_page| { + jump_to_page( + &mut self.page, + &mut self.last_render.rect, + next_page + ) + }) } + 'N' if self.page > 0 => self.rendered[..(self.page)] + .iter() + .rev() + .enumerate() + .find_map(|(idx, p)| { + p.num_results + .is_some_and(|num| num > 0) + .then_some(self.page - (idx + 1)) + }) + .map(|prev_page| { + jump_to_page( + &mut self.page, + &mut self.last_render.rect, + prev_page + ) + }), 'z' if key.modifiers.contains(KeyModifiers::CONTROL) => { // [todo] better error handling here? @@ -686,16 +693,16 @@ impl Tui { 'O' if self.is_kitty => self.update_zoom(|z| z.level = z.level.saturating_sub(1)), 'L' if self.is_kitty => self.update_zoom(|z| { - z.cell_pan_from_left = z.cell_pan_from_left.saturating_add(1) + z.cell_pan_from_left = z.cell_pan_from_left.saturating_add(1); }), 'H' if self.is_kitty => self.update_zoom(|z| { - z.cell_pan_from_left = z.cell_pan_from_left.saturating_sub(1) + z.cell_pan_from_left = z.cell_pan_from_left.saturating_sub(1); }), 'J' if self.is_kitty => self.update_zoom(|z| { - z.cell_pan_from_top = z.cell_pan_from_top.saturating_add(1) + z.cell_pan_from_top = z.cell_pan_from_top.saturating_add(1); }), 'K' if self.is_kitty => self.update_zoom(|z| { - z.cell_pan_from_top = z.cell_pan_from_top.saturating_sub(1) + z.cell_pan_from_top = z.cell_pan_from_top.saturating_sub(1); }), 'G' if self.is_kitty => self.update_zoom(|z| z.cell_pan_from_top = u16::MAX), @@ -819,7 +826,7 @@ impl Tui { #[expect(clippy::unnecessary_wraps)] fn update_zoom(&mut self, f: impl FnOnce(&mut Zoom)) -> Option { if let Some(z) = &mut self.zoom { - f(z) + f(z); } self.last_render.rect = Rect::default(); Some(InputAction::Redraw)