From 725d399d82be41724e859913ab75cbc964d5f060 Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier Date: Mon, 14 Oct 2024 23:19:27 +0200 Subject: [PATCH] some improvements and refactoring on the way --- Cargo.lock | 733 +----------------- Cargo.toml | 14 +- TODO.md | 5 +- crates/television/channels.rs | 6 +- .../television/channels/{grep.rs => text.rs} | 9 +- crates/television/components.rs | 1 - crates/television/main.rs | 1 - crates/television/previewers.rs | 4 +- crates/television/previewers/cache.rs | 2 +- crates/television/previewers/files.rs | 81 +- crates/television/television.rs | 505 +++--------- crates/television/ui.rs | 158 +--- crates/television/ui/input.rs | 3 +- crates/television/ui/input/actions.rs | 30 + crates/television/ui/layout.rs | 114 +++ crates/television/ui/preview.rs | 275 +++++++ crates/television/ui/results.rs | 116 +++ crates/television/utils.rs | 1 - crates/television/utils/strings.rs | 3 + crates/television/utils/ui.rs | 24 - 20 files changed, 721 insertions(+), 1364 deletions(-) rename crates/television/channels/{grep.rs => text.rs} (95%) delete mode 100644 crates/television/components.rs create mode 100644 crates/television/ui/input/actions.rs create mode 100644 crates/television/ui/layout.rs create mode 100644 crates/television/ui/preview.rs create mode 100644 crates/television/ui/results.rs delete mode 100644 crates/television/utils/ui.rs diff --git a/Cargo.lock b/Cargo.lock index 6b807ed..c119731 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -44,12 +44,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aligned-vec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" - [[package]] name = "allocator-api2" version = "0.2.18" @@ -111,35 +105,12 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" -[[package]] -name = "arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" - [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" -[[package]] -name = "arg_enum_proc_macro" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - [[package]] name = "async-trait" version = "0.1.83" @@ -157,29 +128,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" -[[package]] -name = "av1-grain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" -dependencies = [ - "anyhow", - "arrayvec", - "log", - "nom", - "num-rational", - "v_frame", -] - -[[package]] -name = "avif-serialize" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" -dependencies = [ - "arrayvec", -] - [[package]] name = "backtrace" version = "0.3.71" @@ -226,12 +174,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - [[package]] name = "bitflags" version = "1.3.2" @@ -247,12 +189,6 @@ dependencies = [ "serde", ] -[[package]] -name = "bitstream-io" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" - [[package]] name = "block-buffer" version = "0.10.4" @@ -273,36 +209,12 @@ dependencies = [ "serde", ] -[[package]] -name = "built" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" - -[[package]] -name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "bytemuck" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" - [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - [[package]] name = "bytes" version = "1.7.2" @@ -362,8 +274,6 @@ version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -378,16 +288,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -425,7 +325,7 @@ version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "syn 2.0.79", @@ -470,12 +370,6 @@ dependencies = [ "tracing-error", ] -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - [[package]] name = "colorchoice" version = "1.0.2" @@ -732,9 +626,9 @@ dependencies = [ [[package]] name = "devicons" -version = "0.5.4" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4a83fa60a6fd75c04a70ccf71daf61a96eb324f4008627907d232a621641e3" +checksum = "f85ba42bea802686a1532ab64b4b885b2df6e0f931d523b3e64a31ce68f4888d" dependencies = [ "lazy_static", ] @@ -791,12 +685,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" -[[package]] -name = "dyn-clone" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" - [[package]] name = "either" version = "1.13.0" @@ -825,22 +713,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "exr" -version = "1.72.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" -dependencies = [ - "bit_field", - "flume", - "half", - "lebe", - "miniz_oxide 0.7.4", - "rayon-core", - "smallvec", - "zune-inflate", -] - [[package]] name = "eyre" version = "0.6.12" @@ -863,15 +735,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" -[[package]] -name = "fdeflate" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" -dependencies = [ - "simd-adler32", -] - [[package]] name = "filedescriptor" version = "0.8.2" @@ -905,15 +768,6 @@ dependencies = [ "miniz_oxide 0.8.0", ] -[[package]] -name = "flume" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" -dependencies = [ - "spin", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1024,15 +878,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1054,16 +899,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "gif" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" -dependencies = [ - "color_quant", - "weezl", -] - [[package]] name = "gimli" version = "0.28.1" @@ -1577,16 +1412,6 @@ dependencies = [ "regex-syntax 0.8.5", ] -[[package]] -name = "half" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" -dependencies = [ - "cfg-if", - "crunchy", -] - [[package]] name = "hashbrown" version = "0.13.2" @@ -1614,12 +1439,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -1657,12 +1476,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "icy_sixel" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86858ae800284d596cfdefcb0ad435c3493c12f35367431bbe9b2b3858c1155b" - [[package]] name = "ident_case" version = "1.0.1" @@ -1695,57 +1508,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "image" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "num-traits", - "png", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[package]] -name = "image-webp" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" -dependencies = [ - "byteorder-lite", - "quick-error", -] - -[[package]] -name = "imgref" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" - -[[package]] -name = "impl-enum" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd48df611bdfc457ea8c24c8c26e5c9d64857454fa8221da7db463d134371f3" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "indenter" version = "0.3.3" @@ -1781,32 +1543,12 @@ dependencies = [ "syn 2.0.79", ] -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -1847,21 +1589,6 @@ dependencies = [ "jiff-tzdb", ] -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" - [[package]] name = "json5" version = "0.4.1" @@ -1879,29 +1606,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - [[package]] name = "libc" version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" -[[package]] -name = "libfuzzer-sys" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" -dependencies = [ - "arbitrary", - "cc", - "once_cell", -] - [[package]] name = "libredox" version = "0.1.3" @@ -1941,15 +1651,6 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "loop9" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" -dependencies = [ - "imgref", -] - [[package]] name = "lru" version = "0.12.5" @@ -1968,15 +1669,6 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", -] - [[package]] name = "memchr" version = "2.7.4" @@ -2014,7 +1706,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", - "simd-adler32", ] [[package]] @@ -2030,12 +1721,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - [[package]] name = "nom" version = "7.1.3" @@ -2046,12 +1731,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "noop_proc_macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2083,62 +1762,12 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -2341,34 +1970,12 @@ dependencies = [ "time", ] -[[package]] -name = "png" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.0", -] - [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - [[package]] name = "pretty_assertions" version = "1.4.1" @@ -2394,40 +2001,6 @@ version = "28.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79" -[[package]] -name = "profiling" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" -dependencies = [ - "profiling-procmacros", -] - -[[package]] -name = "profiling-procmacros" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" -dependencies = [ - "quote", - "syn 2.0.79", -] - -[[package]] -name = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quick-xml" version = "0.32.0" @@ -2446,36 +2019,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - [[package]] name = "ratatui" version = "0.28.1" @@ -2487,7 +2030,7 @@ dependencies = [ "compact_str", "crossterm", "instability", - "itertools 0.13.0", + "itertools", "lru", "paste", "serde", @@ -2498,70 +2041,6 @@ dependencies = [ "unicode-width 0.1.14", ] -[[package]] -name = "ratatui-image" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de94276254cb20fb7431726875bd2ac6391a6ffc26f4b8e3d23f79d1286b491e" -dependencies = [ - "base64 0.22.1", - "dyn-clone", - "icy_sixel", - "image", - "rand", - "ratatui", - "rustix", -] - -[[package]] -name = "rav1e" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" -dependencies = [ - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", - "av1-grain", - "bitstream-io", - "built", - "cfg-if", - "interpolate_name", - "itertools 0.12.1", - "libc", - "libfuzzer-sys", - "log", - "maybe-rayon", - "new_debug_unreachable", - "noop_proc_macro", - "num-derive", - "num-traits", - "once_cell", - "paste", - "profiling", - "rand", - "rand_chacha", - "simd_helpers", - "system-deps", - "thiserror", - "v_frame", - "wasm-bindgen", -] - -[[package]] -name = "ravif" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" -dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rgb", -] - [[package]] name = "rayon" version = "1.10.0" @@ -2646,15 +2125,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "rgb" -version = "0.8.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" -dependencies = [ - "bytemuck", -] - [[package]] name = "ron" version = "0.8.1" @@ -2835,21 +2305,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "simd_helpers" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] - [[package]] name = "slab" version = "0.4.9" @@ -2875,15 +2330,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -dependencies = [ - "lock_api", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -2920,7 +2366,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -2971,28 +2417,9 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "television" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "better-panic", @@ -3004,11 +2431,8 @@ dependencies = [ "devicons", "directories", "futures", - "fuzzy-matcher", "human-panic", "ignore", - "image", - "impl-enum", "infer", "json5", "lazy_static", @@ -3018,7 +2442,6 @@ dependencies = [ "parking_lot", "pretty_assertions", "ratatui", - "ratatui-image", "regex", "serde", "serde_json", @@ -3028,8 +2451,6 @@ dependencies = [ "syntect", "television-derive", "tokio", - "tokio-stream", - "tokio-util", "toml", "tracing", "tracing-error", @@ -3100,17 +2521,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "tiff" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" -dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", -] - [[package]] name = "time" version = "0.3.36" @@ -3197,30 +2607,6 @@ dependencies = [ "syn 2.0.79", ] -[[package]] -name = "tokio-stream" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "toml" version = "0.8.19" @@ -3387,7 +2773,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" dependencies = [ - "itertools 0.13.0", + "itertools", "unicode-segmentation", "unicode-width 0.1.14", ] @@ -3430,17 +2816,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "v_frame" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" -dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", -] - [[package]] name = "valuable" version = "0.1.0" @@ -3488,12 +2863,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" @@ -3536,67 +2905,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wasm-bindgen" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" -dependencies = [ - "cfg-if", - "once_cell", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 2.0.79", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.79", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" - -[[package]] -name = "weezl" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" - [[package]] name = "winapi" version = "0.3.9" @@ -3806,7 +3114,6 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "byteorder", "zerocopy-derive", ] @@ -3820,27 +3127,3 @@ dependencies = [ "quote", "syn 2.0.79", ] - -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zune-jpeg" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" -dependencies = [ - "zune-core", -] diff --git a/Cargo.toml b/Cargo.toml index 600d531..c0978a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,14 +41,10 @@ color-eyre = "0.6.3" config = "0.14.0" crossterm = { version = "0.28.1", features = ["serde"] } derive_deref = "1.1.1" -devicons = "0.5.4" +devicons = "0.6.8" directories = "5.0.1" futures = "0.3.30" -fuzzy-matcher = "0.3.7" -human-panic = "2.0.1" ignore = "0.4.23" -image = "0.25.2" -impl-enum = "0.3.1" infer = "0.16.0" json5 = "0.4.1" lazy_static = "1.5.0" @@ -56,9 +52,7 @@ libc = "0.2.158" nucleo = "0.5.0" nucleo-matcher = "0.3.1" parking_lot = "0.12.3" -pretty_assertions = "1.4.0" ratatui = { version = "0.28.1", features = ["serde", "macros"] } -ratatui-image = "1.0.5" regex = "1.10.6" serde = { version = "1.0.208", features = ["derive"] } serde_json = "1.0.125" @@ -67,13 +61,13 @@ strip-ansi-escapes = "0.2.0" strum = { version = "0.26.3", features = ["derive"] } syntect = "5.2.0" tokio = { version = "1.39.3", features = ["full"] } -tokio-stream = "0.1.16" -tokio-util = "0.7.11" toml = "0.8.19" tracing = "0.1.40" tracing-error = "0.2.0" tracing-subscriber = { version = "0.3.18", features = ["env-filter", "serde"] } unicode-width = "0.2.0" +human-panic = "2.0.2" +pretty_assertions = "1.4.1" [build-dependencies] @@ -88,7 +82,7 @@ debug = "none" strip = "symbols" debug-assertions = false overflow-checks = false -lto = "thin" +lto = "fat" panic = "abort" [target.'cfg(target_os = "macos")'.dependencies] diff --git a/TODO.md b/TODO.md index a62c6f5..3fc5540 100644 --- a/TODO.md +++ b/TODO.md @@ -6,9 +6,6 @@ - [x] piping output to another command - [x] piping custom entries from stdin (e.g. `ls | tv`, what bout choosing previewers in that case? Some AUTO mode?) -## bugs -- [x] sanitize input (tabs, \0, etc) (see https://github.com/autobib/nucleo-picker/blob/d51dec9efd523e88842c6eda87a19c0a492f4f36/src/lib.rs#L212-L227) - ## improvements - [x] async finder initialization - [x] async finder search @@ -21,6 +18,8 @@ - [x] only ever read a portion of the file for the temp preview - [ ] make layout an attribute of the channel? - [x] I feel like the finder abstraction is a superfluous layer, maybe just use the channel directly? +- [ ] support for images is implemented but do we really want that in the core? it's quite heavy +- [ ] use an icon for the prompt ## feature ideas - [ ] some sort of iterative fuzzy file explorer (preview contents of folders on the right, enter to go in etc.) maybe diff --git a/crates/television/channels.rs b/crates/television/channels.rs index 1aad3ed..bfb8da4 100644 --- a/crates/television/channels.rs +++ b/crates/television/channels.rs @@ -4,7 +4,7 @@ use television_derive::CliChannel; mod alias; mod env; mod files; -mod grep; +mod text; mod stdin; /// The interface that all television channels must implement. @@ -12,7 +12,7 @@ mod stdin; /// # Important /// The `TelevisionChannel` requires the `Send` trait to be implemented as /// well. This is necessary to allow the channels to be used in a -/// multi-threaded environment. +/// multithreaded environment. /// /// # Methods /// - `find`: Find entries that match the given pattern. This method does not @@ -87,7 +87,7 @@ pub trait TelevisionChannel: Send { pub enum AvailableChannels { Env(env::Channel), Files(files::Channel), - Grep(grep::Channel), + Text(text::Channel), Stdin(stdin::Channel), Alias(alias::Channel), } diff --git a/crates/television/channels/grep.rs b/crates/television/channels/text.rs similarity index 95% rename from crates/television/channels/grep.rs rename to crates/television/channels/text.rs index 09a8e57..6d8fdd1 100644 --- a/crates/television/channels/grep.rs +++ b/crates/television/channels/text.rs @@ -10,7 +10,7 @@ use std::{ sync::Arc, }; -use tracing::info; +use tracing::{debug, info}; use super::TelevisionChannel; use crate::entry::Entry; @@ -182,13 +182,18 @@ async fn load_candidates(path: PathBuf, injector: Injector) { match maybe_line { Ok(l) => { line_number += 1; + let line = preprocess_line(&l); + if line.is_empty() { + debug!("Empty line"); + continue; + } let candidate = CandidateLine::new( entry .path() .strip_prefix(¤t_dir) .unwrap() .to_path_buf(), - preprocess_line(&l), + line, line_number, ); // Send the line via the async channel diff --git a/crates/television/components.rs b/crates/television/components.rs deleted file mode 100644 index 8b13789..0000000 --- a/crates/television/components.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/television/main.rs b/crates/television/main.rs index 315bb09..53e8531 100644 --- a/crates/television/main.rs +++ b/crates/television/main.rs @@ -12,7 +12,6 @@ mod action; mod app; mod channels; mod cli; -mod components; mod config; mod entry; mod errors; diff --git a/crates/television/previewers.rs b/crates/television/previewers.rs index 5c74c22..bc7f3d6 100644 --- a/crates/television/previewers.rs +++ b/crates/television/previewers.rs @@ -11,7 +11,7 @@ mod files; pub use basic::BasicPreviewer; pub use env::EnvVarPreviewer; pub use files::FilePreviewer; -use ratatui_image::protocol::StatefulProtocol; +//use ratatui_image::protocol::StatefulProtocol; use syntect::highlighting::Style; #[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] @@ -27,7 +27,7 @@ pub enum PreviewContent { Empty, FileTooLarge, HighlightedText(Vec>), - Image(Box), + //Image(Box), Loading, NotSupported, PlainText(Vec), diff --git a/crates/television/previewers/cache.rs b/crates/television/previewers/cache.rs index 47e4f6e..42fd622 100644 --- a/crates/television/previewers/cache.rs +++ b/crates/television/previewers/cache.rs @@ -72,7 +72,7 @@ where const DEFAULT_PREVIEW_CACHE_SIZE: usize = 100; /// A cache for previews. -/// The cache is implemented as a LRU cache with a fixed size. +/// The cache is implemented as an LRU cache with a fixed size. pub struct PreviewCache { entries: HashMap>, ring_set: RingSet, diff --git a/crates/television/previewers/files.rs b/crates/television/previewers/files.rs index 3ff0b17..4b279c9 100644 --- a/crates/television/previewers/files.rs +++ b/crates/television/previewers/files.rs @@ -1,6 +1,6 @@ use color_eyre::Result; -use image::{ImageReader, Rgb}; -use ratatui_image::picker::Picker; +//use image::{ImageReader, Rgb}; +//use ratatui_image::picker::Picker; use std::fs::File; use std::io::{BufRead, BufReader, Read, Seek}; use std::path::{Path, PathBuf}; @@ -12,7 +12,8 @@ use syntect::{ highlighting::{Style, Theme, ThemeSet}, parsing::SyntaxSet, }; -use tracing::{debug, info, warn}; +//use tracing::{debug, info, warn}; +use tracing::{debug, warn}; use crate::entry; use crate::previewers::{Preview, PreviewContent}; @@ -27,16 +28,16 @@ pub struct FilePreviewer { cache: Arc>, syntax_set: Arc, syntax_theme: Arc, - image_picker: Arc>, + //image_picker: Arc>, } impl FilePreviewer { pub fn new() -> Self { let syntax_set = SyntaxSet::load_defaults_nonewlines(); let theme_set = ThemeSet::load_defaults(); - info!("getting image picker"); - let image_picker = get_image_picker(); - info!("got image picker"); + //info!("getting image picker"); + //let image_picker = get_image_picker(); + //info!("got image picker"); FilePreviewer { cache: Arc::new(Mutex::new(PreviewCache::default())), @@ -44,31 +45,31 @@ impl FilePreviewer { syntax_theme: Arc::new( theme_set.themes["base16-ocean.dark"].clone(), ), - image_picker: Arc::new(Mutex::new(image_picker)), + //image_picker: Arc::new(Mutex::new(image_picker)), } } - async fn compute_image_preview(&self, entry: &entry::Entry) { - let cache = self.cache.clone(); - let picker = self.image_picker.clone(); - let entry_c = entry.clone(); - tokio::spawn(async move { - info!("Loading image: {:?}", entry_c.name); - if let Ok(dyn_image) = - ImageReader::open(entry_c.name.clone()).unwrap().decode() - { - let image = picker.lock().await.new_resize_protocol(dyn_image); - let preview = Arc::new(Preview::new( - entry_c.name.clone(), - PreviewContent::Image(image), - )); - cache - .lock() - .await - .insert(entry_c.name.clone(), preview.clone()); - } - }); - } + //async fn compute_image_preview(&self, entry: &entry::Entry) { + // let cache = self.cache.clone(); + // let picker = self.image_picker.clone(); + // let entry_c = entry.clone(); + // tokio::spawn(async move { + // info!("Loading image: {:?}", entry_c.name); + // if let Ok(dyn_image) = + // ImageReader::open(entry_c.name.clone()).unwrap().decode() + // { + // let image = picker.lock().await.new_resize_protocol(dyn_image); + // let preview = Arc::new(Preview::new( + // entry_c.name.clone(), + // PreviewContent::Image(image), + // )); + // cache + // .lock() + // .await + // .insert(entry_c.name.clone(), preview.clone()); + // } + // }); + //} async fn compute_highlighted_text_preview( &self, @@ -213,8 +214,8 @@ impl FilePreviewer { let preview = loading(&entry.name); self.cache_preview(entry.name.clone(), preview.clone()) .await; - // compute the image preview in the background - self.compute_image_preview(entry).await; + //// compute the image preview in the background + //self.compute_image_preview(entry).await; preview } FileType::Other => { @@ -235,15 +236,15 @@ impl FilePreviewer { } } -fn get_image_picker() -> Picker { - let mut picker = match Picker::from_termios() { - Ok(p) => p, - Err(_) => Picker::new((7, 14)), - }; - picker.guess_protocol(); - picker.background_color = Some(Rgb::([255, 0, 255])); - picker -} +//fn get_image_picker() -> Picker { +// let mut picker = match Picker::from_termios() { +// Ok(p) => p, +// Err(_) => Picker::new((7, 14)), +// }; +// picker.guess_protocol(); +// picker.background_color = Some(Rgb::([255, 0, 255])); +// picker +//} /// This should be enough to most standard terminal sizes const TEMP_PLAIN_TEXT_PREVIEW_HEIGHT: usize = 200; diff --git a/crates/television/television.rs b/crates/television/television.rs index 07724e1..7be90b1 100644 --- a/crates/television/television.rs +++ b/crates/television/television.rs @@ -1,30 +1,30 @@ use color_eyre::Result; use futures::executor::block_on; use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style, Stylize}, - text::{Line, Span, Text}, + layout::{ + Alignment, Constraint, Direction, Layout as RatatuiLayout, Rect, + }, + style::{Color, Style}, + text::{Line, Span}, widgets::{ block::{Position, Title}, - Block, BorderType, Borders, ListState, Padding, Paragraph, Wrap, + Block, BorderType, Borders, ListState, Padding, Paragraph, }, Frame, }; -use ratatui_image::StatefulImage; -use std::{collections::HashMap, str::FromStr, sync::Arc}; -use syntect; -use syntect::highlighting::Color as SyntectColor; - +use std::{collections::HashMap, str::FromStr}; use tokio::sync::mpsc::UnboundedSender; use crate::channels::{CliTvChannel, TelevisionChannel}; use crate::entry::{Entry, ENTRY_PLACEHOLDER}; -use crate::previewers::{ - Preview, PreviewContent, Previewer, FILE_TOO_LARGE_MSG, - PREVIEW_NOT_SUPPORTED_MSG, -}; -use crate::ui::input::{Input, InputRequest, StateChanged}; -use crate::ui::{build_results_list, create_layout, get_border_style}; +use crate::previewers::Previewer; +use crate::ui::get_border_style; +use crate::ui::input::actions::InputActionHandler; +use crate::ui::input::Input; +use crate::ui::layout::{Dimensions, Layout}; +use crate::ui::preview::DEFAULT_PREVIEW_TITLE_FG; +use crate::ui::results::build_results_list; +use crate::utils::strings::EMPTY_STRING; use crate::{action::Action, config::Config}; #[derive(PartialEq, Copy, Clone)] @@ -48,14 +48,12 @@ pub struct Television { picker_view_offset: usize, results_area_height: u32, previewer: Previewer, - preview_scroll: Option, - preview_pane_height: u16, + pub preview_scroll: Option, + pub(crate) preview_pane_height: u16, current_preview_total_lines: u16, - meta_paragraph_cache: HashMap>, + pub(crate) meta_paragraph_cache: HashMap>, } -const EMPTY_STRING: &str = ""; - impl Television { #[must_use] pub fn new(cli_channel: CliTvChannel) -> Self { @@ -87,7 +85,7 @@ impl Television { #[must_use] /// # Panics - /// This method will panic if the index doesn't fit into a u32. + /// This method will panic if the index doesn't fit into an u32. pub fn get_selected_entry(&self) -> Option { self.picker_state .selected() @@ -140,10 +138,10 @@ impl Television { self.picker_view_offset.saturating_sub(1); } } else { - self.picker_view_offset = (self + self.picker_view_offset = self .channel .result_count() - .saturating_sub(self.results_area_height - 2)) + .saturating_sub(self.results_area_height - 2) as usize; self.picker_state.select(Some( (self.channel.result_count() as usize).saturating_sub(1), @@ -269,19 +267,10 @@ impl Television { } } -// Misc -const FOUR_SPACES: &str = " "; - // Styles // input const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200); const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150); -// preview -const DEFAULT_PREVIEW_TITLE_FG: Color = Color::Blue; -const DEFAULT_SELECTED_PREVIEW_BG: Color = Color::Rgb(50, 50, 50); -const DEFAULT_PREVIEW_CONTENT_FG: Color = Color::Rgb(150, 150, 180); -const DEFAULT_PREVIEW_GUTTER_FG: Color = Color::Rgb(70, 70, 70); -const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color = Color::Rgb(255, 150, 150); impl Television { /// Register an action handler that can send actions for processing if necessary. @@ -400,11 +389,14 @@ impl Television { /// /// * `Result<()>` - An Ok result or an error. pub fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let (results_area, input_area, preview_title_area, preview_area) = - create_layout(area); + //let layout = Layout::all_panes_centered(Dimensions::default(), area); + let layout = + Layout::results_only_centered(Dimensions::new(40, 60), area); - self.results_area_height = u32::from(results_area.height); - self.preview_pane_height = preview_area.height; + self.results_area_height = u32::from(layout.results.height); + if let Some(preview_window) = layout.preview_window { + self.preview_pane_height = preview_window.height; + } // top left block: results let results_block = Block::default() @@ -427,14 +419,14 @@ impl Television { } let entries = self.channel.results( - (results_area.height - 2).into(), - u32::try_from(self.picker_view_offset).unwrap(), + (layout.results.height - 2).into(), + u32::try_from(self.picker_view_offset)?, ); let results_list = build_results_list(results_block, &entries); frame.render_stateful_widget( results_list, - results_area, + layout.results, &mut self.relative_picker_state, ); @@ -450,11 +442,11 @@ impl Television { .border_style(get_border_style(Pane::Input == self.current_pane)) .style(Style::default()); - let input_block_inner = input_block.inner(input_area); + let input_block_inner = input_block.inner(layout.input); - frame.render_widget(input_block, input_area); + frame.render_widget(input_block, layout.input); - let inner_input_chunks = Layout::default() + let inner_input_chunks = RatatuiLayout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Length(2), @@ -478,7 +470,7 @@ impl Television { let width = inner_input_chunks[1].width.max(3) - 3; let scroll = self.input.visual_scroll(width as usize); let input = Paragraph::new(self.input.value()) - .scroll((0, u16::try_from(scroll).unwrap())) + .scroll((0, u16::try_from(scroll)?)) .block(interactive_input_block) .style(Style::default().fg(DEFAULT_INPUT_FG)) .alignment(Alignment::Left); @@ -508,373 +500,94 @@ impl Television { // Put cursor past the end of the input text inner_input_chunks[1].x + u16::try_from( - (self.input.visual_cursor()).max(scroll) - scroll, - ) - .unwrap(), + self.input.visual_cursor().max(scroll) - scroll, + )?, // Move one line down, from the border to the input line inner_input_chunks[1].y, )); } - // top right block: preview title - let selected_entry = - self.get_selected_entry().unwrap_or(ENTRY_PLACEHOLDER); + if layout.preview_title.is_some() || layout.preview_window.is_some() { + let selected_entry = + self.get_selected_entry().unwrap_or(ENTRY_PLACEHOLDER); + let preview = block_on(self.previewer.preview(&selected_entry)); - let preview = block_on(self.previewer.preview(&selected_entry)); - self.current_preview_total_lines = preview.total_lines(); + if let Some(preview_title_area) = layout.preview_title { + // top right block: preview title + self.current_preview_total_lines = preview.total_lines(); - let mut preview_title_spans = Vec::new(); - if let Some(icon) = &selected_entry.icon { - preview_title_spans.push(Span::styled( - icon.to_string(), - Style::default().fg(Color::from_str(icon.color).unwrap()), - )); - preview_title_spans.push(Span::raw(" ")); - } - preview_title_spans.push(Span::styled( - preview.title.clone(), - Style::default().fg(DEFAULT_PREVIEW_TITLE_FG), - )); - let preview_title = Paragraph::new(Line::from(preview_title_spans)) - .block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(get_border_style(false)), - ) - .alignment(Alignment::Left); - frame.render_widget(preview_title, preview_title_area); - - // file preview - let preview_outer_block = Block::default() - .title( - Title::from(" Preview ") - .position(Position::Top) - .alignment(Alignment::Center), - ) - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(get_border_style(Pane::Preview == self.current_pane)) - .style(Style::default()) - .padding(Padding::right(1)); - - let preview_inner_block = - Block::default().style(Style::default()).padding(Padding { - top: 0, - right: 1, - bottom: 0, - left: 1, - }); - let inner = preview_outer_block.inner(preview_area); - frame.render_widget(preview_outer_block, preview_area); - - if let PreviewContent::Image(img) = &preview.content { - let image_component = StatefulImage::new(None); - frame.render_stateful_widget( - image_component, - inner, - &mut img.clone(), - ); - } else { - let preview_block = self.build_preview_paragraph( - preview_inner_block, - inner, - &preview, - selected_entry - .line_number - // FIXME: this actually might panic in some edge cases - .map(|l| u16::try_from(l).unwrap()), - ); - frame.render_widget(preview_block, inner); - } - Ok(()) - } -} - -impl Television { - const FILL_CHAR_SLANTED: char = '╱'; - const FILL_CHAR_EMPTY: char = ' '; - - fn build_preview_paragraph<'b>( - &'b mut self, - preview_block: Block<'b>, - inner: Rect, - preview: &Arc, - target_line: Option, - ) -> Paragraph<'b> { - self.maybe_init_preview_scroll(target_line, inner.height); - match &preview.content { - PreviewContent::PlainText(content) => { - let mut lines = Vec::new(); - for (i, line) in content.iter().enumerate() { - lines.push(Line::from(vec![ - build_line_number_span(i + 1).style(Style::default().fg( - // FIXME: this actually might panic in some edge cases - if matches!( - target_line, - Some(l) if l == u16::try_from(i).unwrap() + 1 - ) - { - DEFAULT_PREVIEW_GUTTER_SELECTED_FG - } else { - DEFAULT_PREVIEW_GUTTER_FG - }, - )), - Span::styled(" │ ", - Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()), - Span::styled( - line.to_string(), - Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg( - if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) { - DEFAULT_SELECTED_PREVIEW_BG - } else { - Color::Reset - }, - ), - ), - ])); - } - let text = Text::from(lines); - Paragraph::new(text) - .block(preview_block) - .scroll((self.preview_scroll.unwrap_or(0), 0)) - } - PreviewContent::PlainTextWrapped(content) => { - let mut lines = Vec::new(); - for line in content.lines() { - lines.push(Line::styled( - line.to_string(), - Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG), + let mut preview_title_spans = Vec::new(); + if let Some(icon) = &selected_entry.icon { + preview_title_spans.push(Span::styled( + icon.to_string(), + Style::default().fg(Color::from_str(icon.color)?), )); + preview_title_spans.push(Span::raw(" ")); } - let text = Text::from(lines); - Paragraph::new(text) - .block(preview_block) - .wrap(Wrap { trim: true }) - } - PreviewContent::HighlightedText(highlighted_lines) => { - compute_paragraph_from_highlighted_lines( - highlighted_lines, - target_line.map(|l| l as usize), - self.preview_scroll.unwrap_or(0), - self.preview_pane_height, - ) - .block(preview_block) - .alignment(Alignment::Left) - .scroll((self.preview_scroll.unwrap_or(0), 0)) + preview_title_spans.push(Span::styled( + preview.title.clone(), + Style::default().fg(DEFAULT_PREVIEW_TITLE_FG), + )); + let preview_title = + Paragraph::new(Line::from(preview_title_spans)) + .block( + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(get_border_style(false)), + ) + .alignment(Alignment::Left); + frame.render_widget(preview_title, preview_title_area); } - // meta - PreviewContent::Loading => self - .build_meta_preview_paragraph( - inner, - "Loading...", - Self::FILL_CHAR_EMPTY, - ) - .block(preview_block) - .alignment(Alignment::Left) - .style(Style::default().add_modifier(Modifier::ITALIC)), - PreviewContent::NotSupported => self - .build_meta_preview_paragraph( - inner, - PREVIEW_NOT_SUPPORTED_MSG, - Self::FILL_CHAR_SLANTED, - ) - .block(preview_block) - .alignment(Alignment::Left) - .style(Style::default().add_modifier(Modifier::ITALIC)), - PreviewContent::FileTooLarge => self - .build_meta_preview_paragraph( - inner, - FILE_TOO_LARGE_MSG, - Self::FILL_CHAR_SLANTED, - ) - .block(preview_block) - .alignment(Alignment::Left) - .style(Style::default().add_modifier(Modifier::ITALIC)), - _ => Paragraph::new(Text::raw(EMPTY_STRING)), - } - } - - fn maybe_init_preview_scroll( - &mut self, - target_line: Option, - height: u16, - ) { - if self.preview_scroll.is_none() { - self.preview_scroll = - Some(target_line.unwrap_or(0).saturating_sub(height / 3)); - } - } - fn build_meta_preview_paragraph<'a>( - &mut self, - inner: Rect, - message: &str, - fill_char: char, - ) -> Paragraph<'a> { - if let Some(paragraph) = self.meta_paragraph_cache.get(message) { - return paragraph.clone(); - } - let message_len = message.len(); - let fill_char_str = fill_char.to_string(); - let fill_line = fill_char_str.repeat(inner.width as usize); - - // Build the paragraph content with slanted lines and center the custom message - let mut lines = Vec::new(); - - // Calculate the vertical center - let vertical_center = inner.height as usize / 2; - let horizontal_padding = (inner.width as usize - message_len) / 2 - 4; - - // Fill the paragraph with slanted lines and insert the centered custom message - for i in 0..inner.height { - if i as usize == vertical_center { - // Center the message horizontally in the middle line - let line = format!( - "{} {} {}", - fill_char_str.repeat(horizontal_padding), - message, - fill_char_str.repeat( - inner.width as usize - - horizontal_padding - - message_len - ) - ); - lines.push(Line::from(line)); - } else if i as usize + 1 == vertical_center - || (i as usize).saturating_sub(1) == vertical_center - { - let line = format!( - "{} {} {}", - fill_char_str.repeat(horizontal_padding), - " ".repeat(message_len), - fill_char_str.repeat( - inner.width as usize - - horizontal_padding - - message_len + if let Some(preview_area) = layout.preview_window { + // file preview + let preview_outer_block = Block::default() + .title( + Title::from(" Preview ") + .position(Position::Top) + .alignment(Alignment::Center), ) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + .border_style(get_border_style( + Pane::Preview == self.current_pane, + )) + .style(Style::default()) + .padding(Padding::right(1)); + + let preview_inner_block = Block::default() + .style(Style::default()) + .padding(Padding { + top: 0, + right: 1, + bottom: 0, + left: 1, + }); + let inner = preview_outer_block.inner(preview_area); + frame.render_widget(preview_outer_block, preview_area); + + //if let PreviewContent::Image(img) = &preview.content { + // let image_component = StatefulImage::new(None); + // frame.render_stateful_widget( + // image_component, + // inner, + // &mut img.clone(), + // ); + //} else { + let preview_block = self.build_preview_paragraph( + preview_inner_block, + inner, + &preview, + selected_entry + .line_number + // FIXME: this actually might panic in some edge cases + .map(|l| u16::try_from(l).unwrap()), ); - lines.push(Line::from(line)); - } else { - lines.push(Line::from(fill_line.clone())); + frame.render_widget(preview_block, inner); + //} } } - - // Create a paragraph with the generated content - let p = Paragraph::new(Text::from(lines)); - self.meta_paragraph_cache - .insert(message.to_string(), p.clone()); - p - } -} - -/// This makes the `Action` type compatible with the `Input` logic. -pub trait InputActionHandler { - // Handle Key event. - fn handle_action(&mut self, action: &Action) -> Option; -} - -impl InputActionHandler for Input { - /// Handle Key event. - fn handle_action(&mut self, action: &Action) -> Option { - match action { - Action::AddInputChar(c) => { - self.handle(InputRequest::InsertChar(*c)) - } - Action::DeletePrevChar => { - self.handle(InputRequest::DeletePrevChar) - } - Action::DeleteNextChar => { - self.handle(InputRequest::DeleteNextChar) - } - Action::GoToPrevChar => self.handle(InputRequest::GoToPrevChar), - Action::GoToNextChar => self.handle(InputRequest::GoToNextChar), - Action::GoToInputStart => self.handle(InputRequest::GoToStart), - Action::GoToInputEnd => self.handle(InputRequest::GoToEnd), - _ => None, - } - } -} - -fn build_line_number_span<'a>(line_number: usize) -> Span<'a> { - Span::from(format!("{line_number:5} ")) -} - -fn compute_paragraph_from_highlighted_lines( - highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>], - line_specifier: Option, - scroll: u16, - preview_pane_height: u16, -) -> Paragraph<'static> { - let preview_lines: Vec = highlighted_lines - .iter() - .enumerate() - .map(|(i, l)| { - if i < scroll as usize - || i >= (scroll + preview_pane_height) as usize - { - return Line::from(Span::raw(EMPTY_STRING)); - } - let line_number = - build_line_number_span(i + 1).style(Style::default().fg( - if line_specifier.is_some() - && i == line_specifier.unwrap() - 1 - { - DEFAULT_PREVIEW_GUTTER_SELECTED_FG - } else { - DEFAULT_PREVIEW_GUTTER_FG - }, - )); - Line::from_iter( - std::iter::once(line_number) - .chain(std::iter::once(Span::styled( - " │ ", - Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim(), - ))) - .chain(l.iter().cloned().map(|sr| { - convert_syn_region_to_span( - &(sr.0, sr.1.replace('\t', FOUR_SPACES)), - if line_specifier.is_some() - && i == line_specifier.unwrap() - 1 - { - Some(SyntectColor { - r: 50, - g: 50, - b: 50, - a: 255, - }) - } else { - None - }, - ) - })), - ) - }) - .collect(); - - Paragraph::new(preview_lines) -} - -fn convert_syn_region_to_span<'a>( - syn_region: &(syntect::highlighting::Style, String), - background: Option, -) -> Span<'a> { - let mut style = Style::default() - .fg(convert_syn_color_to_ratatui_color(syn_region.0.foreground)); - if let Some(background) = background { - style = style.bg(convert_syn_color_to_ratatui_color(background)); + Ok(()) } - style = match syn_region.0.font_style { - syntect::highlighting::FontStyle::BOLD => style.bold(), - syntect::highlighting::FontStyle::ITALIC => style.italic(), - syntect::highlighting::FontStyle::UNDERLINE => style.underlined(), - _ => style, - }; - Span::styled(syn_region.1.clone(), style) -} - -fn convert_syn_color_to_ratatui_color( - color: syntect::highlighting::Color, -) -> ratatui::style::Color { - ratatui::style::Color::Rgb(color.r, color.g, color.b) } diff --git a/crates/television/ui.rs b/crates/television/ui.rs index 254faf3..2a268d1 100644 --- a/crates/television/ui.rs +++ b/crates/television/ui.rs @@ -1,26 +1,10 @@ -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Style, Stylize}, - text::{Line, Span}, - widgets::{Block, List, ListDirection}, -}; -use std::str::FromStr; - -use crate::entry::Entry; -use crate::utils::strings::{next_char_boundary, slice_at_char_boundaries}; -use crate::utils::ui::centered_rect; +use ratatui::style::{Color, Style, Stylize}; pub mod input; +pub mod results; +pub mod preview; +pub mod layout; -// UI size -const UI_WIDTH_PERCENT: u16 = 90; -const UI_HEIGHT_PERCENT: u16 = 90; - -// Styles -// results -const DEFAULT_RESULT_NAME_FG: Color = Color::Blue; -const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150); -const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow; // input //const DEFAULT_INPUT_FG: Color = Color::Rgb(200, 200, 200); //const DEFAULT_RESULTS_COUNT_FG: Color = Color::Rgb(150, 150, 150); @@ -40,137 +24,3 @@ pub fn get_border_style(focused: bool) -> Style { } } -pub fn create_layout(area: Rect) -> (Rect, Rect, Rect, Rect) { - let main_block = centered_rect(UI_WIDTH_PERCENT, UI_HEIGHT_PERCENT, area); - - // split the main block into two vertical chunks - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(main_block); - - // left block: results + input field - let left_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(10), Constraint::Length(3)]) - .split(chunks[0]); - - // right block: preview title + preview - let right_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(3), Constraint::Min(10)]) - .split(chunks[1]); - - ( - left_chunks[0], - left_chunks[1], - right_chunks[0], - right_chunks[1], - ) -} - -pub fn build_results_list<'a, 'b>( - results_block: Block<'b>, - entries: &'a [Entry], -) -> List<'a> -where - 'b: 'a, -{ - List::new(entries.iter().map(|entry| { - let mut spans = Vec::new(); - // optional icon - if let Some(icon) = &entry.icon { - spans.push(Span::styled( - icon.to_string(), - Style::default().fg(Color::from_str(icon.color).unwrap()), - )); - spans.push(Span::raw(" ")); - } - // entry name - if let Some(name_match_ranges) = &entry.name_match_ranges { - let mut last_match_end = 0; - for (start, end) in name_match_ranges - .iter() - .map(|(s, e)| (*s as usize, *e as usize)) - { - spans.push(Span::styled( - slice_at_char_boundaries( - &entry.name, - last_match_end, - start, - ), - Style::default() - .fg(DEFAULT_RESULT_NAME_FG) - .bold() - .italic(), - )); - spans.push(Span::styled( - slice_at_char_boundaries(&entry.name, start, end), - Style::default().fg(Color::Red).bold().italic(), - )); - last_match_end = end; - } - spans.push(Span::styled( - &entry.name[next_char_boundary(&entry.name, last_match_end)..], - Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), - )); - } else { - spans.push(Span::styled( - entry.display_name(), - Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), - )); - } - // optional line number - if let Some(line_number) = entry.line_number { - spans.push(Span::styled( - format!(":{line_number}"), - Style::default().fg(DEFAULT_RESULT_LINE_NUMBER_FG), - )); - } - // optional preview - if let Some(preview) = &entry.value { - spans.push(Span::raw(": ")); - - if let Some(preview_match_ranges) = &entry.value_match_ranges { - if !preview_match_ranges.is_empty() { - let mut last_match_end = 0; - for (start, end) in preview_match_ranges - .iter() - .map(|(s, e)| (*s as usize, *e as usize)) - { - spans.push(Span::styled( - slice_at_char_boundaries( - preview, - last_match_end, - start, - ), - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - spans.push(Span::styled( - slice_at_char_boundaries(preview, start, end), - Style::default().fg(Color::Red), - )); - last_match_end = end; - } - spans.push(Span::styled( - &preview[next_char_boundary( - preview, - preview_match_ranges.last().unwrap().1 as usize, - )..], - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - } - } else { - spans.push(Span::styled( - preview, - Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), - )); - } - } - Line::from(spans) - })) - .direction(ListDirection::BottomToTop) - .highlight_style(Style::default().bg(Color::Rgb(50, 50, 50))) - .highlight_symbol("> ") - .block(results_block) -} diff --git a/crates/television/ui/input.rs b/crates/television/ui/input.rs index eed1bfa..0d31e05 100644 --- a/crates/television/ui/input.rs +++ b/crates/television/ui/input.rs @@ -1,4 +1,5 @@ pub mod backend; +pub mod actions; /// Input requests are used to change the input state. /// @@ -356,7 +357,7 @@ impl Input { /// Get the scroll position with account for multispace characters. pub fn visual_scroll(&self, width: usize) -> usize { - let scroll = (self.visual_cursor()).max(width) - width; + let scroll = self.visual_cursor().max(width) - width; let mut uscroll = 0; let mut chars = self.value().chars(); diff --git a/crates/television/ui/input/actions.rs b/crates/television/ui/input/actions.rs new file mode 100644 index 0000000..c0cd7a4 --- /dev/null +++ b/crates/television/ui/input/actions.rs @@ -0,0 +1,30 @@ +use crate::action::Action; +use crate::ui::input::{Input, InputRequest, StateChanged}; + +/// This makes the `Action` type compatible with the `Input` logic. +pub trait InputActionHandler { + // Handle Key event. + fn handle_action(&mut self, action: &Action) -> Option; +} + +impl InputActionHandler for Input { + /// Handle Key event. + fn handle_action(&mut self, action: &Action) -> Option { + match action { + Action::AddInputChar(c) => { + self.handle(InputRequest::InsertChar(*c)) + } + Action::DeletePrevChar => { + self.handle(InputRequest::DeletePrevChar) + } + Action::DeleteNextChar => { + self.handle(InputRequest::DeleteNextChar) + } + Action::GoToPrevChar => self.handle(InputRequest::GoToPrevChar), + Action::GoToNextChar => self.handle(InputRequest::GoToNextChar), + Action::GoToInputStart => self.handle(InputRequest::GoToStart), + Action::GoToInputEnd => self.handle(InputRequest::GoToEnd), + _ => None, + } + } +} \ No newline at end of file diff --git a/crates/television/ui/layout.rs b/crates/television/ui/layout.rs new file mode 100644 index 0000000..b495112 --- /dev/null +++ b/crates/television/ui/layout.rs @@ -0,0 +1,114 @@ +use ratatui::layout; +use ratatui::layout::{Constraint, Direction, Rect}; + +pub struct Dimensions { + pub x: u16, + pub y: u16, +} + +impl Dimensions { + pub fn new(x: u16, y: u16) -> Self { + Self { x, y } + } +} + +impl Default for Dimensions { + fn default() -> Self { + Self::new(UI_WIDTH_PERCENT, UI_HEIGHT_PERCENT) + } +} + +pub struct Layout { + pub results: Rect, + pub input: Rect, + pub preview_title: Option, + pub preview_window: Option, +} + +impl Layout { + pub fn new( + results: Rect, + input: Rect, + preview_title: Option, + preview_window: Option, + ) -> Self { + Self { + results, + input, + preview_title, + preview_window, + } + } + + /// TODO: add diagram + pub fn all_panes_centered(dimensions: Dimensions, area: Rect) -> Self { + let main_block = centered_rect(dimensions.x, dimensions.y, area); + // split the main block into two vertical chunks + let chunks = layout::Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(50), + Constraint::Percentage(50), + ]) + .split(main_block); + + // left block: results + input field + let left_chunks = layout::Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(10), Constraint::Length(3)]) + .split(chunks[0]); + + // right block: preview title + preview + let right_chunks = layout::Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(3), Constraint::Min(10)]) + .split(chunks[1]); + + Self::new( + left_chunks[0], + left_chunks[1], + Some(right_chunks[0]), + Some(right_chunks[1]), + ) + } + + /// TODO: add diagram + pub fn results_only_centered(dimensions: Dimensions, area: Rect) -> Self { + let main_block = centered_rect(dimensions.x, dimensions.y, area); + // split the main block into two vertical chunks + let chunks = layout::Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(10), Constraint::Length(3)]) + .split(main_block); + + Self::new(chunks[0], chunks[1], None, None) + } +} + +/// helper function to create a centered rect using up certain percentage of the available rect `r` +fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { + // Cut the given rectangle into three vertical pieces + let popup_layout = layout::Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage((100 - percent_y) / 2), + Constraint::Percentage(percent_y), + Constraint::Percentage((100 - percent_y) / 2), + ]) + .split(r); + + // Then cut the middle vertical piece into three width-wise pieces + layout::Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage((100 - percent_x) / 2), + Constraint::Percentage(percent_x), + Constraint::Percentage((100 - percent_x) / 2), + ]) + .split(popup_layout[1])[1] // Return the middle chunk +} + +// UI size +const UI_WIDTH_PERCENT: u16 = 90; +const UI_HEIGHT_PERCENT: u16 = 90; + diff --git a/crates/television/ui/preview.rs b/crates/television/ui/preview.rs new file mode 100644 index 0000000..3a81726 --- /dev/null +++ b/crates/television/ui/preview.rs @@ -0,0 +1,275 @@ +use ratatui::prelude::{Color, Line, Modifier, Span, Style, Stylize, Text}; +use ratatui::widgets::{Block, Paragraph, Wrap}; +use ratatui::layout::{Alignment, Rect}; +use std::sync::Arc; +use syntect::highlighting::Color as SyntectColor; +use crate::previewers::{Preview, PreviewContent, FILE_TOO_LARGE_MSG, PREVIEW_NOT_SUPPORTED_MSG}; +use crate::television::Television; +use crate::utils::strings::{EMPTY_STRING, FOUR_SPACES}; + +// preview +pub const DEFAULT_PREVIEW_TITLE_FG: Color = Color::Blue; +const DEFAULT_SELECTED_PREVIEW_BG: Color = Color::Rgb(50, 50, 50); +const DEFAULT_PREVIEW_CONTENT_FG: Color = Color::Rgb(150, 150, 180); +const DEFAULT_PREVIEW_GUTTER_FG: Color = Color::Rgb(70, 70, 70); +const DEFAULT_PREVIEW_GUTTER_SELECTED_FG: Color = Color::Rgb(255, 150, 150); + +impl Television { + const FILL_CHAR_SLANTED: char = '╱'; + const FILL_CHAR_EMPTY: char = ' '; + + pub fn build_preview_paragraph<'b>( + &'b mut self, + preview_block: Block<'b>, + inner: Rect, + preview: &Arc, + target_line: Option, + ) -> Paragraph<'b> { + self.maybe_init_preview_scroll(target_line, inner.height); + match &preview.content { + PreviewContent::PlainText(content) => { + let mut lines = Vec::new(); + for (i, line) in content.iter().enumerate() { + lines.push(Line::from(vec![ + build_line_number_span(i + 1).style(Style::default().fg( + // FIXME: this actually might panic in some edge cases + if matches!( + target_line, + Some(l) if l == u16::try_from(i).unwrap() + 1 + ) + { + DEFAULT_PREVIEW_GUTTER_SELECTED_FG + } else { + DEFAULT_PREVIEW_GUTTER_FG + }, + )), + Span::styled(" │ ", + Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim()), + Span::styled( + line.to_string(), + Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG).bg( + if matches!(target_line, Some(l) if l == u16::try_from(i).unwrap() + 1) { + DEFAULT_SELECTED_PREVIEW_BG + } else { + Color::Reset + }, + ), + ), + ])); + } + let text = Text::from(lines); + Paragraph::new(text) + .block(preview_block) + .scroll((self.preview_scroll.unwrap_or(0), 0)) + } + PreviewContent::PlainTextWrapped(content) => { + let mut lines = Vec::new(); + for line in content.lines() { + lines.push(Line::styled( + line.to_string(), + Style::default().fg(DEFAULT_PREVIEW_CONTENT_FG), + )); + } + let text = Text::from(lines); + Paragraph::new(text) + .block(preview_block) + .wrap(Wrap { trim: true }) + } + PreviewContent::HighlightedText(highlighted_lines) => { + compute_paragraph_from_highlighted_lines( + highlighted_lines, + target_line.map(|l| l as usize), + self.preview_scroll.unwrap_or(0), + self.preview_pane_height, + ) + .block(preview_block) + .alignment(Alignment::Left) + .scroll((self.preview_scroll.unwrap_or(0), 0)) + } + // meta + PreviewContent::Loading => self + .build_meta_preview_paragraph( + inner, + "Loading...", + Self::FILL_CHAR_EMPTY, + ) + .block(preview_block) + .alignment(Alignment::Left) + .style(Style::default().add_modifier(Modifier::ITALIC)), + PreviewContent::NotSupported => self + .build_meta_preview_paragraph( + inner, + PREVIEW_NOT_SUPPORTED_MSG, + Self::FILL_CHAR_SLANTED, + ) + .block(preview_block) + .alignment(Alignment::Left) + .style(Style::default().add_modifier(Modifier::ITALIC)), + PreviewContent::FileTooLarge => self + .build_meta_preview_paragraph( + inner, + FILE_TOO_LARGE_MSG, + Self::FILL_CHAR_SLANTED, + ) + .block(preview_block) + .alignment(Alignment::Left) + .style(Style::default().add_modifier(Modifier::ITALIC)), + _ => Paragraph::new(Text::raw(EMPTY_STRING)), + } + } + + pub fn maybe_init_preview_scroll( + &mut self, + target_line: Option, + height: u16, + ) { + if self.preview_scroll.is_none() { + self.preview_scroll = + Some(target_line.unwrap_or(0).saturating_sub(height / 3)); + } + } + + pub fn build_meta_preview_paragraph<'a>( + &mut self, + inner: Rect, + message: &str, + fill_char: char, + ) -> Paragraph<'a> { + if let Some(paragraph) = self.meta_paragraph_cache.get(message) { + return paragraph.clone(); + } + let message_len = message.len(); + let fill_char_str = fill_char.to_string(); + let fill_line = fill_char_str.repeat(inner.width as usize); + + // Build the paragraph content with slanted lines and center the custom message + let mut lines = Vec::new(); + + // Calculate the vertical center + let vertical_center = inner.height as usize / 2; + let horizontal_padding = (inner.width as usize - message_len) / 2 - 4; + + // Fill the paragraph with slanted lines and insert the centered custom message + for i in 0..inner.height { + if i as usize == vertical_center { + // Center the message horizontally in the middle line + let line = format!( + "{} {} {}", + fill_char_str.repeat(horizontal_padding), + message, + fill_char_str.repeat( + inner.width as usize + - horizontal_padding + - message_len + ) + ); + lines.push(Line::from(line)); + } else if i as usize + 1 == vertical_center + || (i as usize).saturating_sub(1) == vertical_center + { + let line = format!( + "{} {} {}", + fill_char_str.repeat(horizontal_padding), + " ".repeat(message_len), + fill_char_str.repeat( + inner.width as usize + - horizontal_padding + - message_len + ) + ); + lines.push(Line::from(line)); + } else { + lines.push(Line::from(fill_line.clone())); + } + } + + // Create a paragraph with the generated content + let p = Paragraph::new(Text::from(lines)); + self.meta_paragraph_cache + .insert(message.to_string(), p.clone()); + p + } +} + +fn build_line_number_span<'a>(line_number: usize) -> Span<'a> { + Span::from(format!("{line_number:5} ")) +} + +fn compute_paragraph_from_highlighted_lines( + highlighted_lines: &[Vec<(syntect::highlighting::Style, String)>], + line_specifier: Option, + scroll: u16, + preview_pane_height: u16, +) -> Paragraph<'static> { + let preview_lines: Vec = highlighted_lines + .iter() + .enumerate() + .map(|(i, l)| { + if i < scroll as usize + || i >= (scroll + preview_pane_height) as usize + { + return Line::from(Span::raw(EMPTY_STRING)); + } + let line_number = + build_line_number_span(i + 1).style(Style::default().fg( + if line_specifier.is_some() + && i == line_specifier.unwrap() - 1 + { + DEFAULT_PREVIEW_GUTTER_SELECTED_FG + } else { + DEFAULT_PREVIEW_GUTTER_FG + }, + )); + Line::from_iter( + std::iter::once(line_number) + .chain(std::iter::once(Span::styled( + " │ ", + Style::default().fg(DEFAULT_PREVIEW_GUTTER_FG).dim(), + ))) + .chain(l.iter().cloned().map(|sr| { + convert_syn_region_to_span( + &(sr.0, sr.1.replace('\t', FOUR_SPACES)), + if line_specifier.is_some() + && i == line_specifier.unwrap() - 1 + { + Some(SyntectColor { + r: 50, + g: 50, + b: 50, + a: 255, + }) + } else { + None + }, + ) + })), + ) + }) + .collect(); + + Paragraph::new(preview_lines) +} + +fn convert_syn_region_to_span<'a>( + syn_region: &(syntect::highlighting::Style, String), + background: Option, +) -> Span<'a> { + let mut style = Style::default() + .fg(convert_syn_color_to_ratatui_color(syn_region.0.foreground)); + if let Some(background) = background { + style = style.bg(convert_syn_color_to_ratatui_color(background)); + } + style = match syn_region.0.font_style { + syntect::highlighting::FontStyle::BOLD => style.bold(), + syntect::highlighting::FontStyle::ITALIC => style.italic(), + syntect::highlighting::FontStyle::UNDERLINE => style.underlined(), + _ => style, + }; + Span::styled(syn_region.1.clone(), style) +} + +fn convert_syn_color_to_ratatui_color( + color: syntect::highlighting::Color, +) -> ratatui::style::Color { + ratatui::style::Color::Rgb(color.r, color.g, color.b) +} \ No newline at end of file diff --git a/crates/television/ui/results.rs b/crates/television/ui/results.rs new file mode 100644 index 0000000..7435346 --- /dev/null +++ b/crates/television/ui/results.rs @@ -0,0 +1,116 @@ +use ratatui::prelude::{Color, Line, Span, Style, Stylize}; +use ratatui::widgets::{Block, List, ListDirection}; +use std::str::FromStr; +use crate::entry::Entry; +use crate::utils::strings::{next_char_boundary, slice_at_char_boundaries}; + +// Styles +const DEFAULT_RESULT_NAME_FG: Color = Color::Blue; +const DEFAULT_RESULT_PREVIEW_FG: Color = Color::Rgb(150, 150, 150); +const DEFAULT_RESULT_LINE_NUMBER_FG: Color = Color::Yellow; + +pub fn build_results_list<'a, 'b>( + results_block: Block<'b>, + entries: &'a [Entry], +) -> List<'a> +where + 'b: 'a, +{ + List::new(entries.iter().map(|entry| { + let mut spans = Vec::new(); + // optional icon + if let Some(icon) = &entry.icon { + spans.push(Span::styled( + icon.to_string(), + Style::default().fg(Color::from_str(icon.color).unwrap()), + )); + spans.push(Span::raw(" ")); + } + // entry name + if let Some(name_match_ranges) = &entry.name_match_ranges { + let mut last_match_end = 0; + for (start, end) in name_match_ranges + .iter() + .map(|(s, e)| (*s as usize, *e as usize)) + { + spans.push(Span::styled( + slice_at_char_boundaries( + &entry.name, + last_match_end, + start, + ), + Style::default() + .fg(DEFAULT_RESULT_NAME_FG) + .bold() + .italic(), + )); + spans.push(Span::styled( + slice_at_char_boundaries(&entry.name, start, end), + Style::default().fg(Color::Red).bold().italic(), + )); + last_match_end = end; + } + spans.push(Span::styled( + &entry.name[next_char_boundary(&entry.name, last_match_end)..], + Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), + )); + } else { + spans.push(Span::styled( + entry.display_name(), + Style::default().fg(DEFAULT_RESULT_NAME_FG).bold().italic(), + )); + } + // optional line number + if let Some(line_number) = entry.line_number { + spans.push(Span::styled( + format!(":{line_number}"), + Style::default().fg(DEFAULT_RESULT_LINE_NUMBER_FG), + )); + } + // optional preview + if let Some(preview) = &entry.value { + spans.push(Span::raw(": ")); + + if let Some(preview_match_ranges) = &entry.value_match_ranges { + if !preview_match_ranges.is_empty() { + let mut last_match_end = 0; + for (start, end) in preview_match_ranges + .iter() + .map(|(s, e)| (*s as usize, *e as usize)) + { + spans.push(Span::styled( + slice_at_char_boundaries( + preview, + last_match_end, + start, + ), + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + spans.push(Span::styled( + slice_at_char_boundaries(preview, start, end), + Style::default().fg(Color::Red), + )); + last_match_end = end; + } + spans.push(Span::styled( + &preview[next_char_boundary( + preview, + preview_match_ranges.last().unwrap().1 as usize, + )..], + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + } + } else { + spans.push(Span::styled( + preview, + Style::default().fg(DEFAULT_RESULT_PREVIEW_FG), + )); + } + } + Line::from(spans) + })) + .direction(ListDirection::BottomToTop) + .highlight_style(Style::default().bg(Color::Rgb(50, 50, 50))) + .highlight_symbol("> ") + .block(results_block) +} \ No newline at end of file diff --git a/crates/television/utils.rs b/crates/television/utils.rs index e87b2a3..d11f5e9 100644 --- a/crates/television/utils.rs +++ b/crates/television/utils.rs @@ -1,4 +1,3 @@ pub mod files; pub mod indices; pub mod strings; -pub mod ui; diff --git a/crates/television/utils/strings.rs b/crates/television/utils/strings.rs index 645c1ab..cd44c32 100644 --- a/crates/television/utils/strings.rs +++ b/crates/television/utils/strings.rs @@ -52,6 +52,9 @@ lazy_static! { static ref NULL_SYMBOL: char = char::from_u32(0x2400).unwrap(); } +pub const EMPTY_STRING: &str = ""; +pub const FOUR_SPACES: &str = " "; + const SPACE_CHARACTER: char = ' '; const TAB_CHARACTER: char = '\t'; const LINE_FEED_CHARACTER: char = '\x0A'; diff --git a/crates/television/utils/ui.rs b/crates/television/utils/ui.rs deleted file mode 100644 index c2e0f7d..0000000 --- a/crates/television/utils/ui.rs +++ /dev/null @@ -1,24 +0,0 @@ -use ratatui::layout::{Constraint, Direction, Layout, Rect}; - -/// helper function to create a centered rect using up certain percentage of the available rect `r` -pub fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect { - // Cut the given rectangle into three vertical pieces - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Percentage((100 - percent_y) / 2), - Constraint::Percentage(percent_y), - Constraint::Percentage((100 - percent_y) / 2), - ]) - .split(r); - - // Then cut the middle vertical piece into three width-wise pieces - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage((100 - percent_x) / 2), - Constraint::Percentage(percent_x), - Constraint::Percentage((100 - percent_x) / 2), - ]) - .split(popup_layout[1])[1] // Return the middle chunk -}