mirror of
https://github.com/itsjunetime/tdf.git
synced 2026-06-02 08:01:47 -04:00
New kitty image backend (#74)
* Initial attempt at supporting new backend for kitty images * it's almost working !! * it almost basically works * yaaayyyy it works * Use github kittage * Uhhhh various improvements from kittage and psx-shm * Remove logging * incorporate recovering from deleted images * Make it work correctly with ghostty image eviction too * fall back to stdout if shms don't work * Make help page work again * zooming basically does what you'd expect now * yay zooming woohoo * clean up top and bottom rendering * Only allow zooming in kitty * Add debug logging and fix cursor placement after image display * yaaaay zooming out once you're already zoomed in and respecting kitty's limits for how big of an image to display * mmmm maybe it's finally ready to merge... * Update deps * Switch around list of items on changelog * fmt * Small fixes to avoid panic and allow zooming back in after zooming out
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
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
debug.log
|
||||||
|
|||||||
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
- Update ratatui(-image) dependencies
|
- Update to new `kittage` backend for kitty-protocol-supporting terminals (fixes many issues and improves performance significantly, see [the PR](https://github.com/itsjunetime/tdf/pull/74))
|
||||||
- Use new mupdf search API for slightly better performance
|
- Use new mupdf search API for slightly better performance
|
||||||
|
- Update ratatui(-image) dependencies
|
||||||
|
- Allow specification of default white and black colors for rendered pdfs
|
||||||
- Pause rendering every once in a while while there's a search term to enable searching across the entire document more quickly
|
- Pause rendering every once in a while while there's a search term to enable searching across the entire document more quickly
|
||||||
- Fix an issue with missing search highlights
|
- Fix an issue with missing search highlights
|
||||||
|
|
||||||
|
|||||||
Generated
+177
-21
@@ -41,6 +41,21 @@ version = "0.2.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-tzdata"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anes"
|
name = "anes"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
@@ -386,6 +401,18 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"num-traits",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ciborium"
|
name = "ciborium"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -594,9 +621,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "criterion"
|
name = "criterion"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
|
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anes",
|
"anes",
|
||||||
"cast",
|
"cast",
|
||||||
@@ -618,12 +645,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "criterion-plot"
|
name = "criterion-plot"
|
||||||
version = "0.5.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cast",
|
"cast",
|
||||||
"itertools 0.10.5",
|
"itertools 0.13.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -968,6 +995,19 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flexi_logger"
|
||||||
|
version = "0.31.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "759bfa52db036a2db54f0b5f0ff164efa249b3014720459c5ea4198380c529bc"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"log",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"regex",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "float-ord"
|
name = "float-ord"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -1331,6 +1371,30 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.63"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icy_sixel"
|
name = "icy_sixel"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@@ -1446,15 +1510,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itertools"
|
|
||||||
version = "0.10.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -1518,6 +1573,24 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kittage"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/itsjunetime/kittage.git#d872c44f7fc1d3a9f5f1efdc710c300f7ea31d9f"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"crossterm",
|
||||||
|
"futures-core",
|
||||||
|
"image",
|
||||||
|
"log",
|
||||||
|
"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 +1771,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"
|
||||||
@@ -1855,6 +1937,15 @@ version = "2.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -2291,6 +2382,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"
|
||||||
@@ -2903,10 +3003,13 @@ dependencies = [
|
|||||||
"criterion",
|
"criterion",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"csscolorparser 0.7.2",
|
"csscolorparser 0.7.2",
|
||||||
|
"flexi_logger",
|
||||||
"flume",
|
"flume",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"image",
|
"image",
|
||||||
"itertools 0.14.0",
|
"kittage",
|
||||||
|
"log",
|
||||||
|
"memmap2",
|
||||||
"mimalloc",
|
"mimalloc",
|
||||||
"mupdf",
|
"mupdf",
|
||||||
"nix 0.30.1",
|
"nix 0.30.1",
|
||||||
@@ -3591,7 +3694,7 @@ version = "0.58.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core 0.58.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3601,13 +3704,26 @@ version = "0.58.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement 0.58.0",
|
||||||
"windows-interface",
|
"windows-interface 0.58.0",
|
||||||
"windows-result",
|
"windows-result 0.2.0",
|
||||||
"windows-strings",
|
"windows-strings 0.1.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||||
|
dependencies = [
|
||||||
|
"windows-implement 0.60.0",
|
||||||
|
"windows-interface 0.59.1",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result 0.3.4",
|
||||||
|
"windows-strings 0.4.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -3619,6 +3735,17 @@ dependencies = [
|
|||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-implement"
|
||||||
|
version = "0.60.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -3630,6 +3757,17 @@ dependencies = [
|
|||||||
"syn 2.0.104",
|
"syn 2.0.104",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-interface"
|
||||||
|
version = "0.59.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@@ -3645,16 +3783,34 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-result",
|
"windows-result 0.2.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-strings"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
+9
-2
@@ -29,17 +29,24 @@ ratatui = { git = "https://github.com/itsjunetime/ratatui.git" }
|
|||||||
ratatui-image = { git = "https://github.com/itsjunetime/ratatui-image.git", branch = "vb64_on_personal", default-features = false }
|
ratatui-image = { git = "https://github.com/itsjunetime/ratatui-image.git", branch = "vb64_on_personal", default-features = false }
|
||||||
# ratatui-image = { path = "./ratatui-image", default-features = false }
|
# ratatui-image = { path = "./ratatui-image", default-features = false }
|
||||||
crossterm = { version = "0.29.0", features = ["event-stream"] }
|
crossterm = { version = "0.29.0", features = ["event-stream"] }
|
||||||
|
# crossterm = { path = "../crossterm", features = ["event-stream"] }
|
||||||
image = { version = "0.25.1", features = ["pnm", "rayon", "png"], default-features = false }
|
image = { version = "0.25.1", features = ["pnm", "rayon", "png"], default-features = false }
|
||||||
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"
|
||||||
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", "log"] }
|
||||||
|
kittage = { git = "https://github.com/itsjunetime/kittage.git", features = ["crossterm-tokio", "image-crate", "log"] }
|
||||||
|
memmap2 = "*"
|
||||||
|
|
||||||
|
# logging
|
||||||
|
log = "0.4.27"
|
||||||
|
flexi_logger = "0.31"
|
||||||
|
|
||||||
# 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 }
|
||||||
@@ -57,7 +64,7 @@ epub = ["mupdf/epub"]
|
|||||||
cbz = ["mupdf/cbz"]
|
cbz = ["mupdf/cbz"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.6.0", features = ["async_tokio"] }
|
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
||||||
cpuprofiler = "0.0.4"
|
cpuprofiler = "0.0.4"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
|
|||||||
+7
-2
@@ -87,12 +87,15 @@ pub fn start_rendering_loop(
|
|||||||
};
|
};
|
||||||
to_render_tx.send(RenderNotif::Area(main_area)).unwrap();
|
to_render_tx.send(RenderNotif::Area(main_area)).unwrap();
|
||||||
|
|
||||||
|
let cell_height_px = size.height / size.rows;
|
||||||
|
let cell_width_px = size.width / size.columns;
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
start_rendering(
|
start_rendering(
|
||||||
&str_path,
|
&str_path,
|
||||||
to_main_tx,
|
to_main_tx,
|
||||||
from_main_rx,
|
from_main_rx,
|
||||||
size,
|
cell_height_px,
|
||||||
|
cell_width_px,
|
||||||
tdf::PrerenderLimit::All,
|
tdf::PrerenderLimit::All,
|
||||||
black,
|
black,
|
||||||
white
|
white
|
||||||
@@ -119,7 +122,9 @@ pub fn start_converting_loop(
|
|||||||
to_main_tx,
|
to_main_tx,
|
||||||
from_main_rx,
|
from_main_rx,
|
||||||
picker,
|
picker,
|
||||||
prerender
|
prerender,
|
||||||
|
// just assume shms work for now, who cares
|
||||||
|
true
|
||||||
));
|
));
|
||||||
|
|
||||||
let from_converter_rx = from_converter_rx.into_stream();
|
let from_converter_rx = from_converter_rx.into_stream();
|
||||||
|
|||||||
+107
-23
@@ -1,15 +1,58 @@
|
|||||||
|
use std::{
|
||||||
|
num::{NonZeroU32, NonZeroUsize},
|
||||||
|
time::{SystemTime, UNIX_EPOCH}
|
||||||
|
};
|
||||||
|
|
||||||
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 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},
|
||||||
|
skip::InterleavedAroundWithMax
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MaybeTransferred {
|
||||||
|
NotYet(kittage::image::Image<'static>),
|
||||||
|
Transferred(kittage::ImageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ConvertedImage {
|
||||||
|
Generic(Protocol),
|
||||||
|
Kitty {
|
||||||
|
img: MaybeTransferred,
|
||||||
|
cell_w: u16,
|
||||||
|
cell_h: u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConvertedImage {
|
||||||
|
pub fn w_h(&self) -> (u16, u16) {
|
||||||
|
match self {
|
||||||
|
Self::Generic(prot) => {
|
||||||
|
let a = prot.area();
|
||||||
|
(a.width, a.height)
|
||||||
|
}
|
||||||
|
Self::Kitty {
|
||||||
|
img: _,
|
||||||
|
cell_w,
|
||||||
|
cell_h
|
||||||
|
} => (*cell_w, *cell_h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -24,17 +67,21 @@ pub async fn run_conversion_loop(
|
|||||||
sender: Sender<Result<ConvertedPage, RenderError>>,
|
sender: Sender<Result<ConvertedPage, RenderError>>,
|
||||||
receiver: Receiver<ConverterMsg>,
|
receiver: Receiver<ConverterMsg>,
|
||||||
mut picker: Picker,
|
mut picker: Picker,
|
||||||
prerender: usize
|
prerender: usize,
|
||||||
|
shms_work: bool
|
||||||
) -> 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,
|
||||||
|
shms_work: bool
|
||||||
) -> 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);
|
||||||
@@ -45,13 +92,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)) = (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)))
|
// .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);
|
||||||
};
|
};
|
||||||
@@ -81,17 +134,40 @@ pub async fn run_conversion_loop(
|
|||||||
y: 0
|
y: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// We don't actually want to Crop this image, but we've already
|
let txt_img = match picker.protocol_type() {
|
||||||
// verified (with the ImageSurface stuff) that the image is the correct
|
ProtocolType::Kitty => {
|
||||||
// size for the area given, so to save ratatui the work of having to
|
let rn = SystemTime::now()
|
||||||
// resize it, we tell them to crop it to fit.
|
.duration_since(UNIX_EPOCH)
|
||||||
let txt_img = picker
|
.unwrap_or_default()
|
||||||
.new_protocol(dyn_img, img_area, Resize::None)
|
.as_nanos();
|
||||||
.map_err(|e| {
|
|
||||||
RenderError::Converting(format!(
|
let mut img = if shms_work {
|
||||||
"Couldn't convert DynamicImage to ratatui image: {e}"
|
kittage::image::Image::shm_from(
|
||||||
))
|
dyn_img,
|
||||||
})?;
|
&format!("__tdf_kittage_{pid}_page_{rn}_{page_num}")
|
||||||
|
)
|
||||||
|
.map_err(|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());
|
||||||
|
ConvertedImage::Kitty {
|
||||||
|
img: MaybeTransferred::NotYet(img),
|
||||||
|
cell_w: page_info.img_data.cell_w,
|
||||||
|
cell_h: page_info.img_data.cell_h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ConvertedImage::Generic(
|
||||||
|
picker
|
||||||
|
.new_protocol(dyn_img, img_area, Resize::None)
|
||||||
|
.map_err(|e| {
|
||||||
|
RenderError::Converting(format!(
|
||||||
|
"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 +206,15 @@ 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,
|
||||||
|
shms_work
|
||||||
|
) {
|
||||||
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))?
|
||||||
|
|||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
use std::{io::Write, num::NonZeroU32};
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor::MoveTo,
|
||||||
|
event::EventStream,
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode}
|
||||||
|
};
|
||||||
|
use image::DynamicImage;
|
||||||
|
use kittage::{
|
||||||
|
AsyncInputReader, ImageDimensions, ImageId, NumberOrId, PixelFormat,
|
||||||
|
action::Action,
|
||||||
|
delete::{ClearOrDelete, DeleteConfig, WhichToDelete},
|
||||||
|
display::{CursorMovementPolicy, DisplayConfig, DisplayLocation},
|
||||||
|
error::TransmitError,
|
||||||
|
image::Image,
|
||||||
|
medium::Medium
|
||||||
|
};
|
||||||
|
use ratatui::layout::Position;
|
||||||
|
|
||||||
|
use crate::converter::MaybeTransferred;
|
||||||
|
|
||||||
|
pub struct KittyReadyToDisplay<'tui> {
|
||||||
|
pub img: &'tui mut MaybeTransferred,
|
||||||
|
pub page_num: usize,
|
||||||
|
pub pos: Position,
|
||||||
|
pub display_loc: DisplayLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum KittyDisplay<'tui> {
|
||||||
|
NoChange,
|
||||||
|
ClearImages,
|
||||||
|
DisplayImages(Vec<KittyReadyToDisplay<'tui>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DbgWriter<W: Write> {
|
||||||
|
w: W,
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
buf: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Write> Write for DbgWriter<W> {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
if let Ok(s) = std::str::from_utf8(buf) {
|
||||||
|
self.buf.push_str(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.w.write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
log::debug!("Writing to kitty: {:?}", self.buf);
|
||||||
|
self.buf.clear();
|
||||||
|
}
|
||||||
|
self.w.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_action<'image, 'data, 'es>(
|
||||||
|
action: Action<'image, 'data>,
|
||||||
|
ev_stream: &'es mut EventStream
|
||||||
|
) -> Result<ImageId, TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>> {
|
||||||
|
let writer = DbgWriter {
|
||||||
|
w: std::io::stdout().lock(),
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
buf: String::new()
|
||||||
|
};
|
||||||
|
action
|
||||||
|
.execute_async(writer, ev_stream)
|
||||||
|
.await
|
||||||
|
.map(|(_, i)| i)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn do_shms_work(ev_stream: &mut EventStream) -> bool {
|
||||||
|
let img = DynamicImage::new_rgb8(1, 1);
|
||||||
|
let pid = std::process::id();
|
||||||
|
let Ok(mut k_img) = kittage::image::Image::shm_from(img, &format!("__tdf_kittage_test_{pid}"))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// apparently the terminal won't respond to queries unless they have an Id instead of a number
|
||||||
|
k_img.num_or_id = NumberOrId::Id(NonZeroU32::new(u32::MAX).unwrap());
|
||||||
|
|
||||||
|
enable_raw_mode().unwrap();
|
||||||
|
|
||||||
|
let res = run_action(Action::Query(&k_img), ev_stream).await;
|
||||||
|
|
||||||
|
disable_raw_mode().unwrap();
|
||||||
|
|
||||||
|
res.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn display_kitty_images<'es>(
|
||||||
|
display: KittyDisplay<'_>,
|
||||||
|
ev_stream: &'es mut EventStream
|
||||||
|
) -> Result<
|
||||||
|
(),
|
||||||
|
(
|
||||||
|
Vec<usize>,
|
||||||
|
&'static str,
|
||||||
|
TransmitError<<&'es mut EventStream as AsyncInputReader>::Error>
|
||||||
|
)
|
||||||
|
> {
|
||||||
|
let images = match display {
|
||||||
|
KittyDisplay::NoChange => return Ok(()),
|
||||||
|
KittyDisplay::DisplayImages(_) | KittyDisplay::ClearImages => {
|
||||||
|
run_action(
|
||||||
|
Action::Delete(DeleteConfig {
|
||||||
|
effect: ClearOrDelete::Clear,
|
||||||
|
which: WhichToDelete::All
|
||||||
|
}),
|
||||||
|
ev_stream
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (vec![], "Couldn't clear previous images", e))?;
|
||||||
|
|
||||||
|
let KittyDisplay::DisplayImages(images) = display else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
images
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut err = None;
|
||||||
|
for KittyReadyToDisplay {
|
||||||
|
img,
|
||||||
|
page_num,
|
||||||
|
pos,
|
||||||
|
display_loc
|
||||||
|
} in images
|
||||||
|
{
|
||||||
|
let config = DisplayConfig {
|
||||||
|
location: display_loc,
|
||||||
|
cursor_movement: CursorMovementPolicy::DontMove,
|
||||||
|
..DisplayConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
execute!(std::io::stdout(), MoveTo(pos.x, pos.y)).unwrap();
|
||||||
|
|
||||||
|
log::debug!("going to display img {img:#?}");
|
||||||
|
log::debug!("displaying with config {config:#?}");
|
||||||
|
|
||||||
|
let this_err = match img {
|
||||||
|
MaybeTransferred::NotYet(image) => {
|
||||||
|
let mut fake_image = Image {
|
||||||
|
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 = run_action(
|
||||||
|
Action::TransmitAndDisplay {
|
||||||
|
image: fake_image,
|
||||||
|
config,
|
||||||
|
placement_id: None
|
||||||
|
},
|
||||||
|
ev_stream
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(img_id) => {
|
||||||
|
*img = MaybeTransferred::Transferred(img_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err((page_num, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MaybeTransferred::Transferred(image_id) => run_action(
|
||||||
|
Action::Display {
|
||||||
|
image_id: *image_id,
|
||||||
|
placement_id: *image_id,
|
||||||
|
config
|
||||||
|
},
|
||||||
|
ev_stream
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|e| (page_num, e))
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("this_err is {this_err:#?}");
|
||||||
|
|
||||||
|
if let Err((id, e)) = this_err {
|
||||||
|
let e = err.get_or_insert_with(|| (vec![], e));
|
||||||
|
e.0.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match err {
|
||||||
|
Some((replace, e)) => Err((replace, "Couldn't transfer image to the terminal", e)),
|
||||||
|
None => Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -3,12 +3,57 @@ use std::num::NonZeroUsize;
|
|||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub enum PrerenderLimit {
|
pub enum PrerenderLimit {
|
||||||
All,
|
All,
|
||||||
Limited(NonZeroUsize)
|
Limited(NonZeroUsize)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod converter;
|
pub mod converter;
|
||||||
|
pub mod kitty;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod skip;
|
pub mod skip;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
|
pub enum FitOrFill {
|
||||||
|
Fit,
|
||||||
|
Fill
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ScaledResult {
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
scale_factor: f32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scale_img_for_area(
|
||||||
|
(img_width, img_height): (f32, f32),
|
||||||
|
(area_width, area_height): (f32, f32),
|
||||||
|
fit_or_fill: FitOrFill
|
||||||
|
) -> ScaledResult {
|
||||||
|
// and get its aspect ratio
|
||||||
|
let img_aspect_ratio = img_width / img_height;
|
||||||
|
|
||||||
|
// Then we get the full pixel dimensions of the area provided to us, and the aspect ratio
|
||||||
|
// of that area
|
||||||
|
let area_aspect_ratio = area_width / area_height;
|
||||||
|
|
||||||
|
// and get the ratio that this page would have to be scaled by to fit perfectly within the
|
||||||
|
// area provided to us.
|
||||||
|
// we do this first by comparing the aspec ratio of the page with the aspect ratio of the
|
||||||
|
// area to fit it within. If the aspect ratio of the page is larger, then we need to scale
|
||||||
|
// the width of the page to fill perfectly within the height of the area. Otherwise, we
|
||||||
|
// scale the height to fit perfectly. The dimension that _is not_ scaled to fit perfectly
|
||||||
|
// is scaled by the same factor as the dimension that _is_ scaled perfectly.
|
||||||
|
let scale_factor = match (img_aspect_ratio > area_aspect_ratio, fit_or_fill) {
|
||||||
|
(true, FitOrFill::Fit) | (false, FitOrFill::Fill) => area_width / img_width,
|
||||||
|
(false, FitOrFill::Fit) | (true, FitOrFill::Fill) => area_height / img_height
|
||||||
|
};
|
||||||
|
|
||||||
|
ScaledResult {
|
||||||
|
width: img_width * scale_factor,
|
||||||
|
height: img_height * scale_factor,
|
||||||
|
scale_factor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+183
-82
@@ -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, Stdout, stdout},
|
||||||
num::NonZeroUsize,
|
num::{NonZeroU32, NonZeroUsize},
|
||||||
path::PathBuf
|
path::PathBuf
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -15,14 +15,24 @@ use crossterm::{
|
|||||||
enable_raw_mode, window_size
|
enable_raw_mode, window_size
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
use flexi_logger::FileSpec;
|
||||||
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::{
|
||||||
|
action::Action,
|
||||||
|
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};
|
||||||
use ratatui_image::picker::Picker;
|
use ratatui_image::{
|
||||||
|
FontSize,
|
||||||
|
picker::{Picker, ProtocolType}
|
||||||
|
};
|
||||||
use tdf::{
|
use tdf::{
|
||||||
PrerenderLimit,
|
PrerenderLimit,
|
||||||
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
|
converter::{ConvertedPage, ConverterMsg, run_conversion_loop},
|
||||||
|
kitty::{KittyDisplay, display_kitty_images, do_shms_work, run_action},
|
||||||
renderer::{self, RenderError, RenderInfo, RenderNotif},
|
renderer::{self, RenderError, RenderInfo, RenderNotif},
|
||||||
tui::{BottomMessage, InputAction, MessageSetting, Tui}
|
tui::{BottomMessage, InputAction, MessageSetting, Tui}
|
||||||
};
|
};
|
||||||
@@ -89,8 +99,24 @@ async fn 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(
|
||||||
|
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| {
|
||||||
|
WrappedErr(format!("Couldn't create FileSpec for logger: {e}").into())
|
||||||
|
})?)
|
||||||
|
.start()
|
||||||
|
.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();
|
||||||
@@ -128,68 +154,10 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if window_size.width == 0 || window_size.height == 0 {
|
if window_size.width == 0 || window_size.height == 0 {
|
||||||
// send the command code to get the terminal window size
|
let (w, h) = get_font_size_through_stdio()?;
|
||||||
print!("\x1b[14t");
|
|
||||||
std::io::stdout().flush().unwrap();
|
|
||||||
|
|
||||||
// we need to enable raw mode here since this bit of output won't print a newline; it'll
|
window_size.width = w;
|
||||||
// just print the info it wants to tell us. So we want to get all characters as they come
|
window_size.height = h;
|
||||||
enable_raw_mode().map_err(|e| {
|
|
||||||
WrappedErr(
|
|
||||||
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// read in the returned size until we hit a 't' (which indicates to us it's done)
|
|
||||||
let input_vec = BufReader::new(std::io::stdin())
|
|
||||||
.bytes()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.take_while(|b| *b != b't')
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// and then disable raw mode again in case we return an error in this next section
|
|
||||||
disable_raw_mode().map_err(|e| {
|
|
||||||
WrappedErr(format!("Can't put the terminal back into a normal input state: {e}").into())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let input_line = String::from_utf8(input_vec).map_err(|e| {
|
|
||||||
WrappedErr(
|
|
||||||
format!(
|
|
||||||
"The terminal responded to our request for its font size by providing non-utf8 data: {e}"
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let input_line = input_line
|
|
||||||
.trim_start_matches("\x1b[4")
|
|
||||||
.trim_start_matches(';');
|
|
||||||
|
|
||||||
// it should input it to us as `\e[4;<height>;<width>t`, so we need to split to get the h/w
|
|
||||||
// ignore the first val
|
|
||||||
let mut splits = input_line.split([';', 't']);
|
|
||||||
|
|
||||||
let (Some(h), Some(w)) = (splits.next(), splits.next()) else {
|
|
||||||
return Err(WrappedErr(
|
|
||||||
format!("Terminal responded with unparseable size response '{input_line}'").into()
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
window_size.height = h.parse::<u16>().map_err(|_| {
|
|
||||||
WrappedErr(
|
|
||||||
format!(
|
|
||||||
"Your terminal said its height is {h}, but that is not a 16-bit unsigned integer"
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
window_size.width = w.parse::<u16>().map_err(|_| {
|
|
||||||
WrappedErr(
|
|
||||||
format!(
|
|
||||||
"Your terminal said its width is {w}, but that is not a 16-bit unsigned integer"
|
|
||||||
)
|
|
||||||
.into()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to create `picker` on this thread because if we create it on the `renderer` thread,
|
// We need to create `picker` on this thread because if we create it on the `renderer` thread,
|
||||||
@@ -208,30 +176,47 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
.prerender
|
.prerender
|
||||||
.and_then(NonZeroUsize::new)
|
.and_then(NonZeroUsize::new)
|
||||||
.map_or(PrerenderLimit::All, PrerenderLimit::Limited);
|
.map_or(PrerenderLimit::All, PrerenderLimit::Limited);
|
||||||
|
|
||||||
|
let cell_height_px = window_size.height / window_size.rows;
|
||||||
|
let cell_width_px = window_size.width / window_size.columns;
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
renderer::start_rendering(
|
renderer::start_rendering(
|
||||||
&file_path,
|
&file_path,
|
||||||
render_tx,
|
render_tx,
|
||||||
render_rx,
|
render_rx,
|
||||||
window_size,
|
cell_height_px,
|
||||||
|
cell_width_px,
|
||||||
prerender,
|
prerender,
|
||||||
black,
|
black,
|
||||||
white
|
white
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let ev_stream = crossterm::event::EventStream::new();
|
let font_size = picker.font_size();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
tokio::spawn(run_conversion_loop(to_main, from_main, picker, 20));
|
let is_kitty = picker.protocol_type() == ProtocolType::Kitty;
|
||||||
|
|
||||||
|
let shms_work = is_kitty && do_shms_work(&mut ev_stream).await;
|
||||||
|
|
||||||
|
tokio::spawn(run_conversion_loop(
|
||||||
|
to_main, from_main, picker, 20, shms_work
|
||||||
|
));
|
||||||
|
|
||||||
let file_name = path.file_name().map_or_else(
|
let file_name = path.file_name().map_or_else(
|
||||||
|| "Unknown file".into(),
|
|| "Unknown file".into(),
|
||||||
|n| n.to_string_lossy().to_string()
|
|n| n.to_string_lossy().to_string()
|
||||||
);
|
);
|
||||||
let tui = Tui::new(file_name, flags.max_wide, flags.r_to_l.unwrap_or_default());
|
let tui = Tui::new(
|
||||||
|
file_name,
|
||||||
|
flags.max_wide,
|
||||||
|
flags.r_to_l.unwrap_or_default(),
|
||||||
|
is_kitty
|
||||||
|
);
|
||||||
|
|
||||||
let backend = CrosstermBackend::new(std::io::stdout());
|
let backend = CrosstermBackend::new(std::io::stdout());
|
||||||
let mut term = Terminal::new(backend).map_err(|e| {
|
let mut term = Terminal::new(backend).map_err(|e| {
|
||||||
@@ -258,9 +243,23 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if is_kitty {
|
||||||
|
run_action(
|
||||||
|
Action::Delete(DeleteConfig {
|
||||||
|
effect: ClearOrDelete::Delete,
|
||||||
|
which: WhichToDelete::IdRange(NonZeroU32::new(1).unwrap()..=NonZeroU32::MAX)
|
||||||
|
}),
|
||||||
|
&mut ev_stream
|
||||||
|
)
|
||||||
|
.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(
|
||||||
@@ -273,14 +272,15 @@ 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,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
tui,
|
tui,
|
||||||
&mut term,
|
&mut term,
|
||||||
main_area
|
main_area,
|
||||||
|
font_size
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -300,6 +300,8 @@ async fn main() -> Result<(), WrappedErr> {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
disable_raw_mode().unwrap();
|
disable_raw_mode().unwrap();
|
||||||
|
|
||||||
|
drop(maybe_logger);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,20 +309,22 @@ 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>>,
|
||||||
mut fullscreen: bool,
|
mut fullscreen: bool,
|
||||||
mut tui: Tui,
|
mut tui: Tui,
|
||||||
term: &mut Terminal<CrosstermBackend<Stdout>>,
|
term: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||||
mut main_area: tdf::tui::RenderLayout
|
mut main_area: tdf::tui::RenderLayout,
|
||||||
|
font_size: FontSize
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
loop {
|
loop {
|
||||||
let mut needs_redraw = true;
|
let mut needs_redraw = true;
|
||||||
|
let next_ev = ev_stream.next().fuse();
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
// First we check if we have any keystrokes
|
// First we check if we have any keystrokes
|
||||||
Some(ev) = ev_stream.next().fuse() => {
|
Some(ev) = next_ev => {
|
||||||
// If we can't get user input, just crash.
|
// If we can't get user input, just crash.
|
||||||
let ev = ev.expect("Couldn't get any user input");
|
let ev = ev.expect("Couldn't get any user input");
|
||||||
|
|
||||||
@@ -330,12 +334,15 @@ 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,
|
||||||
|
InputAction::SwitchRenderZoom(f_or_f) => {
|
||||||
|
to_renderer.send(RenderNotif::SwitchFitOrFill(f_or_f)).unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -359,7 +366,12 @@ async fn enter_redraw_loop(
|
|||||||
}
|
}
|
||||||
Some(img_res) = from_converter.next() => {
|
Some(img_res) = from_converter.next() => {
|
||||||
match img_res {
|
match img_res {
|
||||||
Ok(ConvertedPage { page, num, num_results }) => tui.page_ready(page, num, num_results),
|
Ok(ConvertedPage { page, num, num_results }) => {
|
||||||
|
tui.page_ready(page, num, num_results);
|
||||||
|
if num == tui.page {
|
||||||
|
needs_redraw = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
Err(e) => tui.show_error(e),
|
Err(e) => tui.show_error(e),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -368,15 +380,41 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if needs_redraw {
|
if needs_redraw {
|
||||||
|
let mut to_display = KittyDisplay::NoChange;
|
||||||
term.draw(|f| {
|
term.draw(|f| {
|
||||||
tui.render(f, &main_area);
|
to_display = tui.render(f, &main_area, font_size);
|
||||||
})?;
|
})?;
|
||||||
execute!(stdout(), EndSynchronizedUpdate)?;
|
|
||||||
|
let maybe_err = display_kitty_images(to_display, &mut ev_stream).await;
|
||||||
|
|
||||||
|
if let Err((to_replace, err_desc, enum_err)) = maybe_err {
|
||||||
|
match enum_err {
|
||||||
|
// This is the error that kitty & ghostty provide us when they delete an
|
||||||
|
// image due to memory constraints, so if we get it, we just fix it by
|
||||||
|
// re-rendering so it don't display it to the user
|
||||||
|
//
|
||||||
|
// [TODO] maybe when we detect that an image was deleted, we probe the
|
||||||
|
// terminal for the pages around it to see if they were deleted too and if
|
||||||
|
// they were, we re-render them? idk
|
||||||
|
TransmitError::Terminal(TerminalError::NoEntity(_)) => (),
|
||||||
|
_ => tui.set_msg(MessageSetting::Some(BottomMessage::Error(format!(
|
||||||
|
"{err_desc}: {enum_err}"
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
for page_num in to_replace {
|
||||||
|
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)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -424,3 +462,66 @@ fn parse_color_to_i32(cs: &str) -> Result<i32, csscolorparser::ParseColorError>
|
|||||||
let [r, g, b, _] = color.to_rgba8();
|
let [r, g, b, _] = color.to_rgba8();
|
||||||
Ok(i32::from_be_bytes([0, r, g, b]))
|
Ok(i32::from_be_bytes([0, r, g, b]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_font_size_through_stdio() -> Result<(u16, u16), WrappedErr> {
|
||||||
|
// we need to enable raw mode here since this bit of output won't print a newline; it'll
|
||||||
|
// just print the info it wants to tell us. So we want to get all characters as they come
|
||||||
|
enable_raw_mode().map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!("Can't enable raw mode, which is necessary to receive input: {e}").into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// read in the returned size until we hit a 't' (which indicates to us it's done)
|
||||||
|
let input_vec = BufReader::new(std::io::stdin())
|
||||||
|
.bytes()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.take_while(|b| *b != b't')
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// and then disable raw mode again in case we return an error in this next section
|
||||||
|
disable_raw_mode().map_err(|e| {
|
||||||
|
WrappedErr(format!("Can't put the terminal back into a normal input state: {e}").into())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let input_line = String::from_utf8(input_vec).map_err(|e| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"The terminal responded to our request for its font size by providing non-utf8 data: {e}"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let input_line = input_line
|
||||||
|
.trim_start_matches("\x1b[4")
|
||||||
|
.trim_start_matches(';');
|
||||||
|
|
||||||
|
// it should input it to us as `\e[4;<height>;<width>t`, so we need to split to get the h/w
|
||||||
|
// ignore the first val
|
||||||
|
let mut splits = input_line.split([';', 't']);
|
||||||
|
|
||||||
|
let (Some(h), Some(w)) = (splits.next(), splits.next()) else {
|
||||||
|
return Err(WrappedErr(
|
||||||
|
format!("Terminal responded with unparseable size response '{input_line}'").into()
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = h.parse::<u16>().map_err(|_| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"Your terminal said its height is {h}, but that is not a 16-bit unsigned integer"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let w = w.parse::<u16>().map_err(|_| {
|
||||||
|
WrappedErr(
|
||||||
|
format!(
|
||||||
|
"Your terminal said its width is {w}, but that is not a 16-bit unsigned integer"
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((w, h))
|
||||||
|
}
|
||||||
|
|||||||
+94
-67
@@ -1,19 +1,24 @@
|
|||||||
use std::{thread::sleep, time::Duration};
|
use std::{collections::VecDeque, num::NonZeroUsize, thread::sleep, time::Duration};
|
||||||
|
|
||||||
use crossterm::terminal::WindowSize;
|
|
||||||
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::{
|
||||||
|
FitOrFill, PrerenderLimit, ScaledResult, scale_img_for_area, skip::InterleavedAroundWithMax
|
||||||
|
};
|
||||||
|
|
||||||
|
const KITTY_MAX_W_OR_H: f32 = 10_000.0;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum RenderNotif {
|
pub enum RenderNotif {
|
||||||
Area(Rect),
|
Area(Rect),
|
||||||
JumpToPage(usize),
|
JumpToPage(usize),
|
||||||
|
PageNeedsReRender(usize),
|
||||||
Search(String),
|
Search(String),
|
||||||
|
SwitchFitOrFill(FitOrFill),
|
||||||
Reload,
|
Reload,
|
||||||
Invert
|
Invert
|
||||||
}
|
}
|
||||||
@@ -73,12 +78,13 @@ 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>>,
|
||||||
receiver: Receiver<RenderNotif>,
|
receiver: Receiver<RenderNotif>,
|
||||||
size: WindowSize,
|
col_h: u16,
|
||||||
|
col_w: u16,
|
||||||
prerender: PrerenderLimit,
|
prerender: PrerenderLimit,
|
||||||
black: i32,
|
black: i32,
|
||||||
white: i32
|
white: i32
|
||||||
@@ -88,13 +94,14 @@ pub fn start_rendering(
|
|||||||
let mut search_term = None;
|
let mut search_term = None;
|
||||||
|
|
||||||
// And although the font size could theoretically change, we aren't accounting for that right
|
// And although the font size could theoretically change, we aren't accounting for that right
|
||||||
// now, so we just keep this out of the loop.
|
// now, so we just use the values passed in.
|
||||||
let col_w = size.width / size.columns;
|
|
||||||
let col_h = size.height / size.rows;
|
|
||||||
|
|
||||||
let mut stored_doc = None;
|
let mut stored_doc = None;
|
||||||
let mut invert = false;
|
let mut invert = false;
|
||||||
let mut preserved_area = None;
|
let mut preserved_area = None;
|
||||||
|
let mut fit_or_fill = FitOrFill::Fit;
|
||||||
|
|
||||||
|
let mut need_rerender = VecDeque::new();
|
||||||
|
|
||||||
'reload: loop {
|
'reload: loop {
|
||||||
let doc = match Document::open(path) {
|
let doc = match Document::open(path) {
|
||||||
@@ -128,7 +135,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
|
||||||
@@ -137,7 +150,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
|
||||||
@@ -145,7 +158,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
|
||||||
@@ -165,6 +178,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 {
|
||||||
@@ -180,13 +196,24 @@ 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::SwitchFitOrFill(f_or_f) =>
|
||||||
|
if f_or_f != fit_or_fill {
|
||||||
|
fit_or_fill = f_or_f;
|
||||||
|
fill_default(&mut rendered, n_pages.get());
|
||||||
|
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
|
||||||
@@ -216,28 +243,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
|
||||||
@@ -245,15 +265,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
|
||||||
@@ -262,18 +282,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;
|
||||||
@@ -289,6 +300,7 @@ pub fn start_rendering(
|
|||||||
invert,
|
invert,
|
||||||
black,
|
black,
|
||||||
white,
|
white,
|
||||||
|
fit_or_fill,
|
||||||
(area_w, area_h)
|
(area_w, area_h)
|
||||||
) {
|
) {
|
||||||
// If that fn returned Some, that means it needed to be re-rendered for some
|
// If that fn returned Some, that means it needed to be re-rendered for some
|
||||||
@@ -303,6 +315,8 @@ pub fn start_rendering(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
log::debug!("got pixmap for page {page_num} with WxH {w}x{h}");
|
||||||
|
|
||||||
rendered.num_search_found = Some(ctx.result_rects.len());
|
rendered.num_search_found = Some(ctx.result_rects.len());
|
||||||
rendered.successful = true;
|
rendered.successful = true;
|
||||||
|
|
||||||
@@ -312,13 +326,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
|
||||||
@@ -373,7 +396,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;
|
||||||
}
|
}
|
||||||
@@ -399,7 +422,7 @@ pub fn start_rendering(
|
|||||||
// So now we've just *searched* all the pages but not necessarily rendered all of them.
|
// So now we've just *searched* all the pages but not necessarily rendered all of them.
|
||||||
// So if there are any we have yet to render, we need to loop back to the beginning of
|
// So if there are any we have yet to render, we need to loop back to the beginning of
|
||||||
// this loop to continue rendering all of them
|
// this loop to continue rendering all of them
|
||||||
if rendered.iter().any(|r| !r.successful) {
|
if rendered.iter().any(|r| !r.successful) && prerender == PrerenderLimit::All {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,6 +446,7 @@ struct RenderedContext {
|
|||||||
result_rects: Vec<HighlightRect>
|
result_rects: Vec<HighlightRect>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_arguments)]
|
||||||
fn render_single_page_to_ctx(
|
fn render_single_page_to_ctx(
|
||||||
page: &Page,
|
page: &Page,
|
||||||
search_term: Option<&str>,
|
search_term: Option<&str>,
|
||||||
@@ -430,6 +454,7 @@ fn render_single_page_to_ctx(
|
|||||||
invert: bool,
|
invert: bool,
|
||||||
black: i32,
|
black: i32,
|
||||||
white: i32,
|
white: i32,
|
||||||
|
fit_or_fill: FitOrFill,
|
||||||
(area_w, area_h): (f32, f32)
|
(area_w, area_h): (f32, f32)
|
||||||
) -> Result<RenderedContext, mupdf::error::Error> {
|
) -> Result<RenderedContext, mupdf::error::Error> {
|
||||||
let result_rects = match prev_render.num_search_found {
|
let result_rects = match prev_render.num_search_found {
|
||||||
@@ -440,30 +465,21 @@ fn render_single_page_to_ctx(
|
|||||||
|
|
||||||
// then, get the size of the page
|
// then, get the size of the page
|
||||||
let bounds = page.bounds()?;
|
let bounds = page.bounds()?;
|
||||||
let (p_width, p_height) = (bounds.x1 - bounds.x0, bounds.y1 - bounds.y0);
|
let page_dim = (bounds.x1 - bounds.x0, bounds.y1 - bounds.y0);
|
||||||
|
|
||||||
// and get its aspect ratio
|
let scaled = scale_img_for_area(page_dim, (area_w, area_h), fit_or_fill);
|
||||||
let p_aspect_ratio = p_width / p_height;
|
let ScaledResult {
|
||||||
|
width: mut surface_w,
|
||||||
|
height: mut surface_h,
|
||||||
|
mut scale_factor
|
||||||
|
} = scaled;
|
||||||
|
|
||||||
// Then we get the full pixel dimensions of the area provided to us, and the aspect ratio
|
if surface_w > KITTY_MAX_W_OR_H || surface_h > KITTY_MAX_W_OR_H {
|
||||||
// of that area
|
let descale = (surface_w / KITTY_MAX_W_OR_H).max(surface_h / KITTY_MAX_W_OR_H);
|
||||||
let area_aspect_ratio = area_w / area_h;
|
surface_w /= descale;
|
||||||
|
surface_h /= descale;
|
||||||
// and get the ratio that this page would have to be scaled by to fit perfectly within the
|
scale_factor /= descale;
|
||||||
// area provided to us.
|
}
|
||||||
// we do this first by comparing the aspec ratio of the page with the aspect ratio of the
|
|
||||||
// area to fit it within. If the aspect ratio of the page is larger, then we need to scale
|
|
||||||
// the width of the page to fill perfectly within the height of the area. Otherwise, we
|
|
||||||
// scale the height to fit perfectly. The dimension that _is not_ scaled to fit perfectly
|
|
||||||
// is scaled by the same factor as the dimension that _is_ scaled perfectly.
|
|
||||||
let scale_factor = if p_aspect_ratio > area_aspect_ratio {
|
|
||||||
area_w / p_width
|
|
||||||
} else {
|
|
||||||
area_h / p_height
|
|
||||||
};
|
|
||||||
|
|
||||||
let surface_w = p_width * scale_factor;
|
|
||||||
let surface_h = p_height * scale_factor;
|
|
||||||
|
|
||||||
let colorspace = Colorspace::device_rgb();
|
let colorspace = Colorspace::device_rgb();
|
||||||
let matrix = Matrix::new_scale(scale_factor, scale_factor);
|
let matrix = Matrix::new_scale(scale_factor, scale_factor);
|
||||||
@@ -546,3 +562,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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+309
-115
@@ -8,28 +8,32 @@ use crossterm::{
|
|||||||
enable_raw_mode
|
enable_raw_mode
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
use kittage::display::DisplayLocation;
|
||||||
use nix::{
|
use nix::{
|
||||||
sys::signal::{Signal::SIGSTOP, kill},
|
sys::signal::{Signal::SIGSTOP, kill},
|
||||||
unistd::Pid
|
unistd::Pid
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Frame,
|
Frame,
|
||||||
layout::{Constraint, Flex, Layout, Rect},
|
layout::{Constraint, Flex, Layout, Position, Rect},
|
||||||
style::{Color, Style},
|
style::{Color, Style},
|
||||||
symbols::border,
|
symbols::border,
|
||||||
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::{FontSize, Image};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
FitOrFill,
|
||||||
|
converter::{ConvertedImage, MaybeTransferred},
|
||||||
|
kitty::{KittyDisplay, KittyReadyToDisplay},
|
||||||
renderer::{RenderError, fill_default},
|
renderer::{RenderError, fill_default},
|
||||||
skip::Skip
|
skip::Skip
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Tui {
|
pub struct Tui {
|
||||||
name: String,
|
name: String,
|
||||||
page: usize,
|
pub page: usize,
|
||||||
last_render: LastRender,
|
last_render: LastRender,
|
||||||
bottom_msg: BottomMessage,
|
bottom_msg: BottomMessage,
|
||||||
// we use `prev_msg` to, for example, restore the 'search results' message on the bottom after
|
// we use `prev_msg` to, for example, restore the 'search results' message on the bottom after
|
||||||
@@ -37,10 +41,12 @@ pub struct Tui {
|
|||||||
prev_msg: Option<BottomMessage>,
|
prev_msg: Option<BottomMessage>,
|
||||||
rendered: Vec<RenderedInfo>,
|
rendered: Vec<RenderedInfo>,
|
||||||
page_constraints: PageConstraints,
|
page_constraints: PageConstraints,
|
||||||
showing_help_msg: bool
|
showing_help_msg: bool,
|
||||||
|
is_kitty: bool,
|
||||||
|
zoom: Option<Zoom>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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
|
||||||
@@ -69,12 +75,25 @@ struct PageConstraints {
|
|||||||
r_to_l: bool
|
r_to_l: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
struct Zoom {
|
||||||
|
// just how much 'zoom' you have. Doesn't relate to anything specific yet, except that 0 means
|
||||||
|
// it fills the screen (instead of fits)
|
||||||
|
level: i16,
|
||||||
|
// how many terminal-cells worth of content overflow the left side of the screen (and are thus
|
||||||
|
// not displayed)
|
||||||
|
cell_pan_from_left: u16,
|
||||||
|
// how many terminal-cells worth of content overflow the top side of the screen (and are thus
|
||||||
|
// not displayed)
|
||||||
|
cell_pan_from_top: u16
|
||||||
|
}
|
||||||
|
|
||||||
// This seems like a kinda weird struct because it holds two optionals but any representation
|
// This seems like a kinda weird struct because it holds two optionals but any representation
|
||||||
// within it is valid; I think it's the best way to represent it
|
// within it is valid; I think it's the best way to represent it
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct RenderedInfo {
|
pub 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
|
||||||
@@ -89,7 +108,7 @@ pub struct RenderLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Tui {
|
impl Tui {
|
||||||
pub fn new(name: String, max_wide: Option<NonZeroUsize>, r_to_l: bool) -> Tui {
|
pub fn new(name: String, max_wide: Option<NonZeroUsize>, r_to_l: bool, is_kitty: bool) -> Tui {
|
||||||
Self {
|
Self {
|
||||||
name,
|
name,
|
||||||
page: 0,
|
page: 0,
|
||||||
@@ -98,7 +117,9 @@ impl Tui {
|
|||||||
last_render: LastRender::default(),
|
last_render: LastRender::default(),
|
||||||
rendered: vec![],
|
rendered: vec![],
|
||||||
page_constraints: PageConstraints { max_wide, r_to_l },
|
page_constraints: PageConstraints { max_wide, r_to_l },
|
||||||
showing_help_msg: false
|
showing_help_msg: false,
|
||||||
|
is_kitty,
|
||||||
|
zoom: None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,105 +148,27 @@ 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,
|
||||||
|
font_size: FontSize
|
||||||
|
) -> KittyDisplay<'s> {
|
||||||
if self.showing_help_msg {
|
if self.showing_help_msg {
|
||||||
self.render_help_msg(frame);
|
self.render_help_msg(frame);
|
||||||
return;
|
return KittyDisplay::ClearImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((top_area, bottom_area)) = full_layout.top_and_bottom {
|
if let Some(t_and_b) = full_layout.top_and_bottom {
|
||||||
let top_block = Block::new()
|
Self::render_top_and_bottom(
|
||||||
.padding(Padding {
|
t_and_b,
|
||||||
right: 2,
|
self.page,
|
||||||
left: 2,
|
&self.rendered,
|
||||||
..Padding::default()
|
&self.name,
|
||||||
})
|
frame,
|
||||||
.borders(Borders::BOTTOM);
|
&self.bottom_msg
|
||||||
|
);
|
||||||
let top_area = top_block.inner(top_area);
|
|
||||||
|
|
||||||
let page_nums_text = format!("{} / {}", self.page + 1, self.rendered.len());
|
|
||||||
|
|
||||||
let top_layout = Layout::horizontal([
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(page_nums_text.len() as u16)
|
|
||||||
])
|
|
||||||
.split(top_area);
|
|
||||||
|
|
||||||
let title = Span::styled(&self.name, Style::new().fg(Color::Cyan));
|
|
||||||
|
|
||||||
let page_nums = Span::styled(&page_nums_text, Style::new().fg(Color::Cyan));
|
|
||||||
|
|
||||||
frame.render_widget(top_block, top_area);
|
|
||||||
frame.render_widget(title, top_layout[0]);
|
|
||||||
frame.render_widget(page_nums, top_layout[1]);
|
|
||||||
|
|
||||||
let bottom_block = Block::new()
|
|
||||||
.padding(Padding {
|
|
||||||
top: 1,
|
|
||||||
right: 2,
|
|
||||||
left: 2,
|
|
||||||
bottom: 0
|
|
||||||
})
|
|
||||||
.borders(Borders::TOP);
|
|
||||||
let bottom_inside_block = bottom_block.inner(bottom_area);
|
|
||||||
|
|
||||||
frame.render_widget(bottom_block, bottom_area);
|
|
||||||
|
|
||||||
let rendered_str = if !self.rendered.is_empty() {
|
|
||||||
format!(
|
|
||||||
"Rendered: {}%",
|
|
||||||
(self.rendered.iter().filter(|i| i.img.is_some()).count() * 100)
|
|
||||||
/ self.rendered.len()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
let bottom_layout = Layout::horizontal([
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(rendered_str.len() as u16)
|
|
||||||
])
|
|
||||||
.split(bottom_inside_block);
|
|
||||||
|
|
||||||
let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan));
|
|
||||||
frame.render_widget(rendered_span, bottom_layout[1]);
|
|
||||||
|
|
||||||
let (msg_str, color): (Cow<'_, str>, _) = match self.bottom_msg {
|
|
||||||
BottomMessage::Help => ("?: Show help page".into(), Color::Blue),
|
|
||||||
BottomMessage::Error(ref e) => (e.as_str().into(), Color::Red),
|
|
||||||
BottomMessage::Input(ref input_state) => (
|
|
||||||
match input_state {
|
|
||||||
InputCommand::GoToPage(page) => format!("Go to: {page}"),
|
|
||||||
InputCommand::Search(s) => format!("Search: {s}")
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
Color::Blue
|
|
||||||
),
|
|
||||||
BottomMessage::SearchResults(ref term) => {
|
|
||||||
let num_found = self
|
|
||||||
.rendered
|
|
||||||
.iter()
|
|
||||||
.filter_map(|r| r.num_results)
|
|
||||||
.sum::<usize>();
|
|
||||||
let num_searched = self
|
|
||||||
.rendered
|
|
||||||
.iter()
|
|
||||||
.filter(|r| r.num_results.is_some())
|
|
||||||
.count() * 100;
|
|
||||||
(
|
|
||||||
format!(
|
|
||||||
"Results for '{term}': {num_found} (searched: {}%)",
|
|
||||||
num_searched / self.rendered.len()
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
Color::Blue
|
|
||||||
)
|
|
||||||
}
|
|
||||||
BottomMessage::Reloaded => ("Document was reloaded!".into(), Color::Blue)
|
|
||||||
};
|
|
||||||
|
|
||||||
let span = Span::styled(msg_str, Style::new().fg(color));
|
|
||||||
frame.render_widget(span, bottom_layout[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut img_area = full_layout.page_area;
|
let mut img_area = full_layout.page_area;
|
||||||
@@ -237,7 +180,98 @@ 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);
|
||||||
|
KittyDisplay::NoChange
|
||||||
} else {
|
} else {
|
||||||
|
if let Some(ref mut zoom) = self.zoom {
|
||||||
|
// yes this is ugly and I hate it. it's due to the limitations that currently exist
|
||||||
|
// in the borrow checker. Once `-Zpolonius=next` is stabilized, we can rework this
|
||||||
|
// to look like what we expect.
|
||||||
|
// See https://github.com/rust-lang/rfcs/blob/master/text/2094-nll.md#problem-case-3-conditional-control-flow-across-functions
|
||||||
|
// You can also rewrite this to just if an `if let` and run it under
|
||||||
|
// `RUSTFLAGS="-Zpolonius=next"` and see that it works
|
||||||
|
if self.rendered[self.page]
|
||||||
|
.img
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|c| matches!(c, ConvertedImage::Kitty { .. }))
|
||||||
|
{
|
||||||
|
let Some(ConvertedImage::Kitty {
|
||||||
|
ref mut img,
|
||||||
|
cell_w,
|
||||||
|
cell_h
|
||||||
|
}) = self.rendered[self.page].img
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("zoom is now {zoom:#?}");
|
||||||
|
log::debug!("img_area is {img_area:#?}");
|
||||||
|
|
||||||
|
if zoom.level < 0 {
|
||||||
|
img_area = Rect {
|
||||||
|
width: img_area
|
||||||
|
.width
|
||||||
|
.saturating_sub((zoom.level * 2).unsigned_abs())
|
||||||
|
.max(1),
|
||||||
|
x: img_area.x + (zoom.level.unsigned_abs().min(img_area.width / 2)),
|
||||||
|
..img_area
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("after adjustment, img_area is {img_area:#?}");
|
||||||
|
|
||||||
|
// Ugh I don't like this logic. I wish we could simplify it.
|
||||||
|
let img_width = f32::from(cell_w);
|
||||||
|
let img_height = f32::from(cell_h);
|
||||||
|
let img_area_width = f32::from(img_area.width);
|
||||||
|
let img_area_height = f32::from(img_area.height);
|
||||||
|
let available_to_real_width_ratio = img_area_width / img_width;
|
||||||
|
let available_to_real_height_ratio = img_area_height / img_height;
|
||||||
|
|
||||||
|
let (new_cell_width, new_cell_height) =
|
||||||
|
if available_to_real_width_ratio > available_to_real_height_ratio {
|
||||||
|
(img_width, img_area_height / available_to_real_width_ratio)
|
||||||
|
} else {
|
||||||
|
(img_area_width / available_to_real_height_ratio, img_height)
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("new_cell stuff is {new_cell_width}x{new_cell_height}");
|
||||||
|
|
||||||
|
let width = (new_cell_width * f32::from(font_size.0)) as u32;
|
||||||
|
let height = (new_cell_height * f32::from(font_size.1)) as u32;
|
||||||
|
|
||||||
|
self.last_render = LastRender {
|
||||||
|
rect: size,
|
||||||
|
pages_shown: 1,
|
||||||
|
unused_width: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
zoom.cell_pan_from_left = zoom
|
||||||
|
.cell_pan_from_left
|
||||||
|
.min(cell_w.saturating_sub(new_cell_width as u16));
|
||||||
|
zoom.cell_pan_from_top = zoom
|
||||||
|
.cell_pan_from_top
|
||||||
|
.min(cell_h.saturating_sub(new_cell_height as u16));
|
||||||
|
|
||||||
|
return KittyDisplay::DisplayImages(vec![KittyReadyToDisplay {
|
||||||
|
img,
|
||||||
|
page_num: self.page,
|
||||||
|
pos: Position {
|
||||||
|
x: img_area.x,
|
||||||
|
y: img_area.y
|
||||||
|
},
|
||||||
|
display_loc: DisplayLocation {
|
||||||
|
x: u32::from(zoom.cell_pan_from_left) * u32::from(font_size.0),
|
||||||
|
y: u32::from(zoom.cell_pan_from_top) * u32::from(font_size.1),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
columns: img_area.width,
|
||||||
|
rows: img_area.height,
|
||||||
|
..DisplayLocation::default()
|
||||||
|
}
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 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;
|
||||||
// go through our pages, starting at the first one we want to view
|
// go through our pages, starting at the first one we want to view
|
||||||
@@ -254,7 +288,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.w_h().0, 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 +306,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);
|
||||||
|
KittyDisplay::ClearImages
|
||||||
} else {
|
} else {
|
||||||
execute!(stdout(), BeginSynchronizedUpdate).unwrap();
|
execute!(stdout(), BeginSynchronizedUpdate).unwrap();
|
||||||
|
|
||||||
@@ -283,20 +318,50 @@ 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
|
||||||
Self::render_single_page(frame, img, Rect { width, ..img_area });
|
.into_iter()
|
||||||
img_area.x += width;
|
.enumerate()
|
||||||
}
|
.filter_map(|(idx, (width, img))| {
|
||||||
|
let maybe_img =
|
||||||
|
Self::render_single_page(frame, img, Rect { width, ..img_area });
|
||||||
|
img_area.x += width;
|
||||||
|
maybe_img.map(|(img, pos)| KittyReadyToDisplay {
|
||||||
|
img,
|
||||||
|
page_num: idx + self.page,
|
||||||
|
pos,
|
||||||
|
display_loc: DisplayLocation::default()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.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;
|
||||||
|
|
||||||
|
KittyDisplay::DisplayImages(to_display)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_single_page(frame: &mut Frame<'_>, page_img: &mut Protocol, img_area: Rect) {
|
fn render_single_page<'img>(
|
||||||
frame.render_widget(Image::new(page_img), img_area);
|
frame: &mut Frame<'_>,
|
||||||
|
page_img: &'img mut ConvertedImage,
|
||||||
|
img_area: Rect
|
||||||
|
) -> Option<(&'img mut MaybeTransferred, Position)> {
|
||||||
|
match page_img {
|
||||||
|
ConvertedImage::Generic(page_img) => {
|
||||||
|
frame.render_widget(Image::new(page_img), img_area);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
ConvertedImage::Kitty {
|
||||||
|
img,
|
||||||
|
cell_h: _,
|
||||||
|
cell_w: _
|
||||||
|
} => Some((img, Position {
|
||||||
|
x: img_area.x,
|
||||||
|
y: img_area.y
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_loading_in(frame: &mut Frame<'_>, area: Rect) {
|
fn render_loading_in(frame: &mut Frame<'_>, area: Rect) {
|
||||||
@@ -329,7 +394,8 @@ impl Tui {
|
|||||||
|
|
||||||
let old = self.page;
|
let old = self.page;
|
||||||
match change {
|
match change {
|
||||||
PageChange::Next => self.set_page((self.page + diff).min(self.rendered.len() - 1)),
|
PageChange::Next =>
|
||||||
|
self.set_page((self.page + diff).min(self.rendered.len().saturating_sub(1))),
|
||||||
PageChange::Prev => self.set_page(self.page.saturating_sub(diff))
|
PageChange::Prev => self.set_page(self.page.saturating_sub(diff))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +410,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 +418,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.w_h().0;
|
||||||
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 {
|
||||||
@@ -370,10 +436,101 @@ impl Tui {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn page_failed_display(&mut self, page_num: usize) {
|
||||||
|
self.rendered[page_num].img = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn got_num_results_on_page(&mut self, page_num: usize, num_results: usize) {
|
pub fn got_num_results_on_page(&mut self, page_num: usize, num_results: usize) {
|
||||||
self.rendered[page_num].num_results = Some(num_results);
|
self.rendered[page_num].num_results = Some(num_results);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_top_and_bottom(
|
||||||
|
(top_area, bottom_area): (Rect, Rect),
|
||||||
|
page_num: usize,
|
||||||
|
rendered: &[RenderedInfo],
|
||||||
|
doc_name: &str,
|
||||||
|
frame: &mut Frame<'_>,
|
||||||
|
bottom_msg: &BottomMessage
|
||||||
|
) {
|
||||||
|
// use the extra space here to add some padding to the right side
|
||||||
|
let page_nums_text = format!("{} / {} ", page_num + 1, rendered.len());
|
||||||
|
|
||||||
|
let top_block = Block::new()
|
||||||
|
// use this first title to add a bit of padding to the left side
|
||||||
|
.title_top(" ")
|
||||||
|
.title_top(Span::styled(doc_name, Style::new().fg(Color::Cyan)))
|
||||||
|
.title_top(
|
||||||
|
Span::styled(&page_nums_text, Style::new().fg(Color::Cyan))
|
||||||
|
.into_right_aligned_line()
|
||||||
|
)
|
||||||
|
.padding(Padding {
|
||||||
|
bottom: 1,
|
||||||
|
..Padding::default()
|
||||||
|
})
|
||||||
|
.borders(Borders::BOTTOM);
|
||||||
|
|
||||||
|
frame.render_widget(top_block, top_area);
|
||||||
|
|
||||||
|
let bottom_block = Block::new()
|
||||||
|
.padding(Padding {
|
||||||
|
top: 1,
|
||||||
|
right: 2,
|
||||||
|
left: 2,
|
||||||
|
bottom: 0
|
||||||
|
})
|
||||||
|
.borders(Borders::TOP);
|
||||||
|
let bottom_inside_block = bottom_block.inner(bottom_area);
|
||||||
|
|
||||||
|
frame.render_widget(bottom_block, bottom_area);
|
||||||
|
|
||||||
|
let rendered_str = if !rendered.is_empty() {
|
||||||
|
format!(
|
||||||
|
"Rendered: {}%",
|
||||||
|
(rendered.iter().filter(|i| i.img.is_some()).count() * 100) / rendered.len()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
let bottom_layout = Layout::horizontal([
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(rendered_str.len() as u16)
|
||||||
|
])
|
||||||
|
.split(bottom_inside_block);
|
||||||
|
|
||||||
|
let rendered_span = Span::styled(&rendered_str, Style::new().fg(Color::Cyan));
|
||||||
|
frame.render_widget(rendered_span, bottom_layout[1]);
|
||||||
|
|
||||||
|
let (msg_str, color): (Cow<'_, str>, _) = match bottom_msg {
|
||||||
|
BottomMessage::Help => ("?: Show help page".into(), Color::Blue),
|
||||||
|
BottomMessage::Error(e) => (e.as_str().into(), Color::Red),
|
||||||
|
BottomMessage::Input(input_state) => (
|
||||||
|
match input_state {
|
||||||
|
InputCommand::GoToPage(page) => format!("Go to: {page}"),
|
||||||
|
InputCommand::Search(s) => format!("Search: {s}")
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
Color::Blue
|
||||||
|
),
|
||||||
|
BottomMessage::SearchResults(term) => {
|
||||||
|
let num_found = rendered.iter().filter_map(|r| r.num_results).sum::<usize>();
|
||||||
|
let num_searched =
|
||||||
|
rendered.iter().filter(|r| r.num_results.is_some()).count() * 100;
|
||||||
|
(
|
||||||
|
format!(
|
||||||
|
"Results for '{term}': {num_found} (searched: {}%)",
|
||||||
|
num_searched / rendered.len()
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
Color::Blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BottomMessage::Reloaded => ("Document was reloaded!".into(), Color::Blue)
|
||||||
|
};
|
||||||
|
|
||||||
|
let span = Span::styled(msg_str, Style::new().fg(color));
|
||||||
|
frame.render_widget(span, bottom_layout[0]);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_event(&mut self, ev: &Event) -> Option<InputAction> {
|
pub fn handle_event(&mut self, ev: &Event) -> Option<InputAction> {
|
||||||
fn jump_to_page(
|
fn jump_to_page(
|
||||||
page: &mut usize,
|
page: &mut usize,
|
||||||
@@ -488,6 +645,32 @@ impl Tui {
|
|||||||
self.last_render.rect = Rect::default();
|
self.last_render.rect = Rect::default();
|
||||||
Some(InputAction::Redraw)
|
Some(InputAction::Redraw)
|
||||||
}
|
}
|
||||||
|
'z' if self.is_kitty => {
|
||||||
|
let (zoom, f_or_f) = match self.zoom {
|
||||||
|
None => (Some(Zoom::default()), FitOrFill::Fill),
|
||||||
|
Some(_) => (None, FitOrFill::Fit)
|
||||||
|
};
|
||||||
|
self.zoom = zoom;
|
||||||
|
self.last_render.rect = Rect::default();
|
||||||
|
Some(InputAction::SwitchRenderZoom(f_or_f))
|
||||||
|
}
|
||||||
|
'o' if self.is_kitty => self.update_zoom(|z|
|
||||||
|
// TODO: for now, we don't let people zoom in past fill-screen
|
||||||
|
z.level = z.level.saturating_add(1).min(0)),
|
||||||
|
'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)
|
||||||
|
}),
|
||||||
|
'H' if self.is_kitty => self.update_zoom(|z| {
|
||||||
|
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)
|
||||||
|
}),
|
||||||
|
'K' if self.is_kitty => self.update_zoom(|z| {
|
||||||
|
z.cell_pan_from_top = z.cell_pan_from_top.saturating_sub(1)
|
||||||
|
}),
|
||||||
_ => None
|
_ => None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,6 +770,16 @@ impl Tui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// I want this to always return 0 'cause I just use it to return from `Self::handle_event`]
|
||||||
|
#[expect(clippy::unnecessary_wraps)]
|
||||||
|
fn update_zoom(&mut self, f: impl FnOnce(&mut Zoom)) -> Option<InputAction> {
|
||||||
|
if let Some(z) = &mut self.zoom {
|
||||||
|
f(z)
|
||||||
|
}
|
||||||
|
self.last_render.rect = Rect::default();
|
||||||
|
Some(InputAction::Redraw)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn show_error(&mut self, err: RenderError) {
|
pub fn show_error(&mut self, err: RenderError) {
|
||||||
self.set_msg(MessageSetting::Some(BottomMessage::Error(match err {
|
self.set_msg(MessageSetting::Some(BottomMessage::Error(match err {
|
||||||
RenderError::Notify(e) => format!("Auto-reload failed: {e}"),
|
RenderError::Notify(e) => format!("Auto-reload failed: {e}"),
|
||||||
@@ -697,7 +890,8 @@ pub enum InputAction {
|
|||||||
Search(String),
|
Search(String),
|
||||||
QuitApp,
|
QuitApp,
|
||||||
Invert,
|
Invert,
|
||||||
Fullscreen
|
Fullscreen,
|
||||||
|
SwitchRenderZoom(crate::FitOrFill)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
|
|||||||
Reference in New Issue
Block a user