diff --git a/Cargo.lock b/Cargo.lock index 5642630..3b98aff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -308,6 +308,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "built" version = "0.7.7" @@ -344,6 +353,32 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.9.2", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + [[package]] name = "cast" version = "0.3.0" @@ -476,6 +511,15 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "compact_str" version = "0.9.0" @@ -490,6 +534,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console-api" version = "0.8.1" @@ -538,6 +591,20 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "copypasta" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6811e17f81fe246ef2bc553f76b6ee6ab41a694845df1d37e52a92b7bbd38a" +dependencies = [ + "clipboard-win", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "smithay-clipboard", + "x11-clipboard", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -750,6 +817,12 @@ dependencies = [ "phf", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "darling" version = "0.20.11" @@ -870,6 +943,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dwrote" version = "0.11.3" @@ -934,6 +1013,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "euclid" version = "0.22.11" @@ -1158,6 +1243,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1258,6 +1353,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1512,6 +1613,25 @@ dependencies = [ "libc", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itertools" version = "0.12.1" @@ -2035,6 +2155,105 @@ dependencies = [ "libc", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.9.2", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.9.2", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.9.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.9.2", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.9.2", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + [[package]] name = "object" version = "0.36.7" @@ -2056,6 +2275,17 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -2100,6 +2330,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pathfinder_geometry" version = "0.5.1" @@ -2300,6 +2536,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5bd19146350fe804f7cb2669c851c03d69da628803dab0d98018142aaa5d829" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.60.2", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2400,6 +2650,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.40" @@ -2746,6 +3005,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2888,6 +3153,42 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.9.2", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + [[package]] name = "socket2" version = "0.5.10" @@ -3002,6 +3303,7 @@ name = "tdf-viewer" version = "0.4.2" dependencies = [ "console-subscriber", + "copypasta", "cpuprofiler", "criterion", "crossterm", @@ -3017,6 +3319,7 @@ dependencies = [ "mupdf", "nix 0.30.1", "notify", + "open", "ratatui", "ratatui-image", "rayon", @@ -3578,6 +3881,102 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.0.8", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.2", + "rustix 1.0.8", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.9.2", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" +dependencies = [ + "rustix 1.0.8", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.2", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +dependencies = [ + "bitflags 2.9.2", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -3841,6 +4240,21 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3874,6 +4288,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -3886,6 +4306,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -3898,6 +4324,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3922,6 +4354,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -3934,6 +4372,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -3946,6 +4390,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -3958,6 +4408,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -3997,6 +4453,39 @@ dependencies = [ "bitflags 2.9.2", ] +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "gethostname", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + [[package]] name = "xflags" version = "0.4.0-pre.2" @@ -4012,6 +4501,12 @@ version = "0.4.0-pre.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a6d9b56f406f5754a3808524166b6e6bdfe219c0526e490cfc39ecc0582a4e6" +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "yeslogic-fontconfig-sys" version = "6.0.0" diff --git a/Cargo.toml b/Cargo.toml index 12636f8..636d9b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ mimalloc = "0.1.43" nix = { version = "0.30.0", features = ["signal"] } mupdf = { version = "0.5.0", default-features = false, features = ["svg", "system-fonts", "img"] } rayon = { version = "*", default-features = false } +copypasta = "0.10.2" +open = "5.3.2" # kittage = { path = "../kittage/", features = ["crossterm-tokio", "image-crate", "log"] } kittage = { git = "https://github.com/itsjunetime/kittage.git", features = ["crossterm-tokio", "image-crate", "log"] } memmap2 = "*" diff --git a/src/converter.rs b/src/converter.rs index 2170824..69e368c 100644 --- a/src/converter.rs +++ b/src/converter.rs @@ -16,7 +16,7 @@ use ratatui_image::{ use rayon::iter::ParallelIterator; use crate::{ - renderer::{PageInfo, RenderError, fill_default}, + renderer::{Link, PageInfo, RenderError, fill_default}, skip::InterleavedAroundWithMax }; @@ -54,7 +54,8 @@ impl ConvertedImage { pub struct ConvertedPage { pub page: ConvertedImage, pub num: usize, - pub num_results: usize + pub num_results: usize, + pub links: Vec } pub enum ConverterMsg { @@ -174,7 +175,8 @@ pub async fn run_conversion_loop( Ok(Some(ConvertedPage { page: txt_img, num: page_info.page_num, - num_results: page_info.result_rects.len() + num_results: page_info.result_rects.len(), + links: page_info.links.clone() })) } diff --git a/src/main.rs b/src/main.rs index c00cfe4..7d3ae4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -380,8 +380,8 @@ async fn enter_redraw_loop( } Some(img_res) = from_converter.next() => { match img_res { - Ok(ConvertedPage { page, num, num_results }) => { - tui.page_ready(page, num, num_results); + Ok(ConvertedPage { page, num, num_results, links }) => { + tui.page_ready(page, num, num_results, links); if num == tui.page { needs_redraw = true; } diff --git a/src/renderer.rs b/src/renderer.rs index f3a0fb1..ad40628 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -41,7 +41,8 @@ pub enum RenderInfo { pub struct PageInfo { pub img_data: ImageData, pub page_num: usize, - pub result_rects: Vec + pub result_rects: Vec, + pub links: Vec } #[derive(Clone)] @@ -327,7 +328,8 @@ pub fn start_rendering( cell_h: (ctx.surface_h / f32::from(col_h)) as u16 }, page_num, - result_rects: ctx.result_rects + result_rects: ctx.result_rects, + links: ctx.links })))?; } // And if we got an error, then obviously we need to propagate that @@ -443,7 +445,8 @@ struct RenderedContext { pixmap: Pixmap, surface_w: f32, surface_h: f32, - result_rects: Vec + result_rects: Vec, + links: Vec } #[expect(clippy::too_many_arguments)] @@ -512,11 +515,14 @@ fn render_single_page_to_ctx( }) .collect::>(); + let links = extract_page_links(page)?; + Ok(RenderedContext { pixmap, surface_w, surface_h, - result_rects + result_rects, + links }) } @@ -528,6 +534,12 @@ pub struct HighlightRect { pub lr_y: u32 } +#[derive(Clone, Debug)] +pub struct Link { + pub uri: String, + pub target_page: Option +} + #[inline] fn search_page( page: &Page, @@ -563,6 +575,30 @@ fn count_search_results(page: &Page, search_term: &str) -> Result Result, mupdf::error::Error> { + let links = page.links()?; + let mut unique_links = Vec::new(); + let mut seen_uris = std::collections::HashSet::new(); + + for link in links { + // Only include HTTP/HTTPS URLs, skip internal links and empty URIs + if link.uri.starts_with("http") { + let uri = link.uri.clone(); + + // Only add if we haven't seen this URI before + if !seen_uris.contains(&uri) { + seen_uris.insert(uri.clone()); + + let target_page = Some(link.page); + + unique_links.push(Link { uri, target_page }); + } + } + } + + Ok(unique_links) +} + struct PopOnNext<'a> { inner: &'a mut VecDeque } diff --git a/src/tui.rs b/src/tui.rs index b08f950..8a32cc8 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, io::stdout, num::NonZeroUsize}; +use std::{io::stdout, num::NonZeroUsize}; use crossterm::{ event::{Event, KeyCode, KeyModifiers, MouseEventKind}, @@ -27,7 +27,7 @@ use crate::{ FitOrFill, converter::{ConvertedImage, MaybeTransferred}, kitty::{KittyDisplay, KittyReadyToDisplay}, - renderer::{RenderError, fill_default}, + renderer::{Link, RenderError, fill_default}, skip::Skip }; @@ -42,6 +42,8 @@ pub struct Tui { rendered: Vec, page_constraints: PageConstraints, showing_help_msg: bool, + showing_links_menu: bool, + selected_link_index: usize, is_kitty: bool, zoom: Option } @@ -98,7 +100,9 @@ pub struct RenderedInfo { // we haven't checked this page yet // Also this isn't the most efficient representation of this value, but it's accurate, so like // whatever I guess - num_results: Option + num_results: Option, + // Links detected on this page + links: Vec } #[derive(PartialEq)] @@ -118,6 +122,8 @@ impl Tui { rendered: vec![], page_constraints: PageConstraints { max_wide, r_to_l }, showing_help_msg: false, + showing_links_menu: false, + selected_link_index: 0, is_kitty, zoom: None } @@ -160,6 +166,11 @@ impl Tui { return KittyDisplay::ClearImages; } + if self.showing_links_menu { + self.render_links_menu(frame); + return KittyDisplay::ClearImages; + } + if let Some(t_and_b) = full_layout.top_and_bottom { Self::render_top_and_bottom( t_and_b, @@ -318,21 +329,25 @@ impl Tui { self.last_render.unused_width = unused_width; img_area.x += unused_width / 2; - let to_display = page_widths - .into_iter() - .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 { + let mut to_display = Vec::new(); + + for (idx, (width, img)) in page_widths.into_iter().enumerate() { + let page_rect = Rect { width, ..img_area }; + let page_num = idx + self.page; + + // Render the page + let maybe_img = Self::render_single_page(frame, img, page_rect); + + img_area.x += width; + if let Some((img, pos)) = maybe_img { + to_display.push(KittyReadyToDisplay { img, - page_num: idx + self.page, + page_num, pos, display_loc: DisplayLocation::default() - }) - }) - .collect::>(); + }); + } + } // 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 @@ -410,7 +425,13 @@ impl Tui { self.page = self.page.min(n_pages - 1); } - pub fn page_ready(&mut self, img: ConvertedImage, page_num: usize, num_results: usize) { + pub fn page_ready( + &mut self, + img: ConvertedImage, + page_num: usize, + num_results: usize, + links: Vec + ) { // 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 // the last rect marker so that all images are forced to redraw on next render and this one @@ -432,7 +453,8 @@ impl Tui { // number of pages, so the vec will already be cleared self.rendered[page_num] = RenderedInfo { img: Some(img), - num_results: Some(num_results) + num_results: Some(num_results), + links }; } @@ -491,6 +513,10 @@ impl Tui { } else { String::new() }; + + // Check if current page has links + let has_links = rendered.get(page_num).is_some_and(|r| !r.links.is_empty()); + let bottom_layout = Layout::horizontal([ Constraint::Fill(1), Constraint::Length(rendered_str.len() as u16) @@ -500,35 +526,52 @@ impl Tui { 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 { + // Handle different bottom messages and render with appropriate colors + match bottom_msg { + BottomMessage::Help => { + // Create spans with different colors for help message and links info + let mut spans = vec![Span::styled( + "?: Show help page", + Style::new().fg(Color::Blue) + )]; + if has_links { + spans.push(Span::styled(" | ", Style::new().fg(Color::DarkGray))); + spans.push(Span::styled( + format!("Tab: {} links", rendered[page_num].links.len()), + Style::new().fg(Color::Yellow) + )); + } + let help_line = ratatui::text::Line::from(spans); + frame.render_widget(Paragraph::new(help_line), bottom_layout[0]); + } + BottomMessage::Error(e) => { + let span = Span::styled(e.as_str(), Style::new().fg(Color::Red)); + frame.render_widget(span, bottom_layout[0]); + } + BottomMessage::Input(input_state) => { + let msg_str = match input_state { InputCommand::GoToPage(page) => format!("Go to: {page}"), InputCommand::Search(s) => format!("Search: {s}") - } - .into(), - Color::Blue - ), + }; + let span = Span::styled(msg_str, Style::new().fg(Color::Blue)); + frame.render_widget(span, bottom_layout[0]); + } BottomMessage::SearchResults(term) => { let num_found = rendered.iter().filter_map(|r| r.num_results).sum::(); 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 - ) + let msg_str = format!( + "Results for '{term}': {num_found} (searched: {}%)", + num_searched / rendered.len() + ); + let span = Span::styled(msg_str, Style::new().fg(Color::Blue)); + frame.render_widget(span, bottom_layout[0]); } - 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]); + BottomMessage::Reloaded => { + let span = Span::styled("Document was reloaded!", Style::new().fg(Color::Blue)); + frame.render_widget(span, bottom_layout[0]); + } + } } pub fn handle_event(&mut self, ev: &Event) -> Option { @@ -567,6 +610,21 @@ impl Tui { } match c { + 'c' if self.showing_links_menu => { + // Copy the selected link URL to clipboard + if let Some(rendered_info) = self.rendered.get(self.page) { + if let Some(link) = + rendered_info.links.get(self.selected_link_index) + { + // Copy to clipboard using copypasta + use copypasta::{ClipboardContext, ClipboardProvider}; + if let Ok(mut ctx) = ClipboardContext::new() { + let _ = ctx.set_contents(link.uri.clone()); + } + } + } + None + } 'l' => self.change_page(PageChange::Next, ChangeAmount::Single), 'j' => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), 'h' => self.change_page(PageChange::Prev, ChangeAmount::Single), @@ -689,15 +747,34 @@ impl Tui { KeyCode::Down => self.change_page(PageChange::Next, ChangeAmount::WholeScreen), KeyCode::Left => self.change_page(PageChange::Prev, ChangeAmount::Single), KeyCode::Up => self.change_page(PageChange::Prev, ChangeAmount::WholeScreen), - KeyCode::Esc => match (self.showing_help_msg, &self.bottom_msg) { - (false, BottomMessage::Help) => Some(InputAction::QuitApp), + KeyCode::Esc => match ( + self.showing_help_msg, + self.showing_links_menu, + &self.bottom_msg + ) { + (false, false, BottomMessage::Help) => Some(InputAction::QuitApp), _ => { // When we hit escape, we just want to pop off the current message and // show the underlying one. self.set_msg(MessageSetting::Pop); + self.showing_help_msg = false; + self.showing_links_menu = false; + self.selected_link_index = 0; // Reset link selection + // Force a full redraw by resetting the cached rect + self.last_render.rect = Rect::default(); Some(InputAction::Redraw) } }, + KeyCode::Enter if self.showing_links_menu => { + if let Some(rendered_info) = self.rendered.get(self.page) { + if let Some(link) = rendered_info.links.get(self.selected_link_index) { + if link.uri.starts_with("http") { + let _ = open::that(&link.uri); + } + } + } + None // Don't redraw, just execute the command + } KeyCode::Enter => { let mut default = BottomMessage::default(); std::mem::swap(&mut self.bottom_msg, &mut default); @@ -753,6 +830,35 @@ impl Tui { } } } + KeyCode::Tab => { + if self.showing_links_menu { + // Cycle through links when menu is open + if let Some(rendered_info) = self.rendered.get(self.page) { + if !rendered_info.links.is_empty() { + self.selected_link_index = + (self.selected_link_index + 1) % rendered_info.links.len(); + Some(InputAction::Redraw) + } else { + None + } + } else { + None + } + } else { + // Open links menu when not already open + if let Some(rendered_info) = self.rendered.get(self.page) { + if !rendered_info.links.is_empty() { + self.showing_links_menu = true; + self.selected_link_index = 0; + Some(InputAction::Redraw) + } else { + None + } + } else { + None + } + } + } _ => None } } @@ -810,6 +916,7 @@ impl Tui { // mark that we need to re-render the images self.last_render.rect = Rect::default(); self.page = page; + self.selected_link_index = 0; // Reset link selection when changing pages } } @@ -876,6 +983,77 @@ impl Tui { frame.render_widget(block, block_area[1]); frame.render_widget(help_span, block_inner); } + + pub fn render_links_menu(&self, frame: &mut Frame<'_>) { + let frame_area = frame.area(); + frame.render_widget(Clear, frame_area); + + let block = Block::new() + .title("Links in Current Page (Tab: cycle, Enter: open, c: copy, Esc: close)") + .padding(Padding::proportional(1)) + .borders(Borders::ALL) + .border_set(border::ROUNDED) + .border_style(Color::Blue); + + // Get links for the current page + let links_text = if let Some(rendered_info) = self.rendered.get(self.page) { + if rendered_info.links.is_empty() { + "No links found on this page.".to_string() + } else { + let max_link_width = 70; // Reserve space for numbering and selection indicator + let mut text = String::new(); + for (i, link) in rendered_info.links.iter().enumerate() { + let truncated_uri = if link.uri.len() > max_link_width { + format!("{}...", &link.uri[..max_link_width]) + } else { + link.uri.clone() + }; + + let prefix = if i == self.selected_link_index { + format!("> {}. ", i + 1) // Selected item gets ">" prefix + } else { + format!(" {}. ", i + 1) // Normal items get spaces + }; + + text.push_str(&format!("{prefix}{truncated_uri}\n")); + } + text + } + } else { + "Page not yet loaded.".to_string() + }; + + let links_span = Paragraph::new(links_text.as_str()).wrap(Wrap { trim: false }); + + let max_w: u16 = links_text + .lines() + .map(str::len) + .max() + .unwrap_or(30) + .min(80) + .try_into() + .unwrap_or(30); + + let layout = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Length(max_w + 6), + Constraint::Fill(1) + ]) + .split(frame_area); + + let num_lines = links_text.lines().count().max(3); + let block_area = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(u16::try_from(num_lines).unwrap_or(10) + 4), + Constraint::Fill(1) + ]) + .split(layout[1]); + + let block_inner = block.inner(block_area[1]); + + frame.render_widget(block, block_area[1]); + frame.render_widget(links_span, block_inner); + } } static HELP_PAGE: &str = "\