sapling-streampager-0.11.0/.cargo_vcs_info.json0000644000000002020000000000100150520ustar { "git": { "sha1": "a0e78ceebed32baa30abb0b8f58f463c2d0f2d4d" }, "path_in_vcs": "eden/scm/lib/third-party/streampager" }sapling-streampager-0.11.0/Cargo.lock0000644000001132610000000000100130370ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anyhow" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "atomic" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" dependencies = [ "bytemuck", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "bit-set" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bytemuck" version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crossbeam-channel" version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "csscolorparser" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" dependencies = [ "lab", "phf", ] [[package]] name = "deltae" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", ] [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", "windows-sys 0.59.0", ] [[package]] name = "enum_dispatch" version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" dependencies = [ "once_cell", "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys 0.59.0", ] [[package]] name = "euclid" version = "0.22.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" dependencies = [ "num-traits", ] [[package]] name = "fancy-regex" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" dependencies = [ "bit-set", "regex", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filedescriptor" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" dependencies = [ "libc", "thiserror 1.0.69", "winapi", ] [[package]] name = "filetime" version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" dependencies = [ "cfg-if", "libc", "libredox", "windows-sys 0.59.0", ] [[package]] name = "finl_unicode" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94c970b525906eb37d3940083aa65b95e481fc1857d467d13374e1d925cfc163" [[package]] name = "fixedbitset" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fsevent-sys" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" dependencies = [ "libc", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ "cfg-if", "libc", "wasi 0.13.3+wasi-0.2.2", "windows-targets 0.52.6", ] [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "indexmap" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "inotify" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" dependencies = [ "bitflags 1.3.2", "inotify-sys", "libc", ] [[package]] name = "inotify-sys" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" dependencies = [ "libc", ] [[package]] name = "kqueue" version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", ] [[package]] name = "kqueue-sys" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", ] [[package]] name = "lab" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libredox" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.8.0", "libc", "redox_syscall", ] [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "log" version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lru" version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" [[package]] name = "mac_address" version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8836fae9d0d4be2c8b4efcdd79e828a2faa058a90d005abf42f91cac5493a08e" dependencies = [ "nix", "winapi", ] [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memmap2" version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" dependencies = [ "libc", ] [[package]] name = "memmem" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] [[package]] name = "nix" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ "bitflags 2.8.0", "cfg-if", "cfg_aliases", "libc", "memoffset", ] [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "notify" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486" dependencies = [ "bitflags 1.3.2", "crossbeam-channel", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", "mio", "walkdir", "windows-sys 0.45.0", ] [[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.98", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ "num-traits", ] [[package]] name = "pest" version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc" dependencies = [ "memchr", "thiserror 2.0.11", "ucd-trie", ] [[package]] name = "pest_derive" version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e" dependencies = [ "pest", "pest_generator", ] [[package]] name = "pest_generator" version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "pest_meta" version = "2.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea" dependencies = [ "once_cell", "pest", "sha2", ] [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", ] [[package]] name = "phf_codegen" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", ] [[package]] name = "phf_generator" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", ] [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "phf_shared" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher 1.0.1", ] [[package]] name = "proc-macro2" version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "redox_syscall" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ "bitflags 2.8.0", ] [[package]] name = "redox_users" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.15", "libredox", "thiserror 2.0.11", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.59.0", ] [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "sapling-streampager" version = "0.11.0" dependencies = [ "bit-set", "dirs", "enum_dispatch", "indexmap", "lazy_static", "lru", "memmap2", "notify", "regex", "scopeguard", "serde", "smallvec", "tempfile", "terminfo", "termwiz", "thiserror 2.0.11", "toml", "unicode-segmentation", "unicode-width", "vec_map", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "serde_spanned" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] [[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", "digest", ] [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-registry" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] [[package]] name = "siphasher" version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", ] [[package]] name = "terminfo" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" dependencies = [ "fnv", "nom", "phf", "phf_codegen", ] [[package]] name = "termios" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ "libc", ] [[package]] name = "termwiz" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed32af792ae81937cb8640b03eaef737408e5c8feee47b35e8b80c49bcb64524" dependencies = [ "anyhow", "base64", "bitflags 2.8.0", "fancy-regex", "filedescriptor", "finl_unicode", "fixedbitset", "hex", "lazy_static", "libc", "log", "memmem", "nix", "num-derive", "num-traits", "ordered-float", "pest", "pest_derive", "phf", "sha2", "signal-hook", "siphasher 0.3.11", "terminfo", "termios", "thiserror 1.0.69", "ucd-trie", "unicode-segmentation", "vtparse", "wezterm-bidi", "wezterm-blob-leases", "wezterm-color-types", "wezterm-dynamic", "wezterm-input-types", "winapi", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "thiserror-impl" version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", "syn 2.0.98", ] [[package]] name = "toml" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ "atomic", "getrandom 0.2.15", ] [[package]] name = "vec_map" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vtparse" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" dependencies = [ "utf8parse", ] [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "wezterm-bidi" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" dependencies = [ "log", "wezterm-dynamic", ] [[package]] name = "wezterm-blob-leases" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5a5e0adf7eed68976410def849a4bdab6f6e9f6163f152de9cb89deea9e60b" dependencies = [ "getrandom 0.2.15", "mac_address", "once_cell", "sha2", "thiserror 1.0.69", "uuid", ] [[package]] name = "wezterm-color-types" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" dependencies = [ "csscolorparser", "deltae", "lazy_static", "wezterm-dynamic", ] [[package]] name = "wezterm-dynamic" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" dependencies = [ "log", "ordered-float", "strsim", "thiserror 1.0.69", "wezterm-dynamic-derive", ] [[package]] name = "wezterm-dynamic-derive" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "wezterm-input-types" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" dependencies = [ "bitflags 1.3.2", "euclid", "lazy_static", "wezterm-dynamic", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ "windows-targets 0.42.2", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets 0.48.5", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-targets" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm 0.42.2", "windows_aarch64_msvc 0.42.2", "windows_i686_gnu 0.42.2", "windows_i686_msvc 0.42.2", "windows_x86_64_gnu 0.42.2", "windows_x86_64_gnullvm 0.42.2", "windows_x86_64_msvc 0.42.2", ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen-rt" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ "bitflags 2.8.0", ] sapling-streampager-0.11.0/Cargo.toml0000644000000037750000000000100130720ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "sapling-streampager" version = "0.11.0" authors = [ "Mark Juggurnauth-Thomas ", "Facebook Source Control Team ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "streampager is a pager for command output or large files" readme = false keywords = [ "less", "more", "pager", ] categories = [ "command-line-utilities", "text-processing", ] license = "MIT" [lib] name = "streampager" path = "src/lib.rs" [dependencies.bit-set] version = "0.5" [dependencies.dirs] version = "6.0" [dependencies.enum_dispatch] version = "0.3.13" [dependencies.indexmap] version = "2.2.6" [dependencies.lazy_static] version = "1.5" [dependencies.lru] version = "=0.12.4" default-features = false [dependencies.memmap2] version = "0.5.10" [dependencies.notify] version = "5" optional = true [dependencies.regex] version = "1.11.1" [dependencies.scopeguard] version = "1.2.0" [dependencies.serde] version = "1.0.185" features = ["derive"] [dependencies.smallvec] version = "1.13.2" default-features = false [dependencies.tempfile] version = "3.15" [dependencies.terminfo] version = "0.9" [dependencies.termwiz] version = "0.23" [dependencies.thiserror] version = "2" [dependencies.toml] version = "0.8.19" [dependencies.unicode-segmentation] version = "1.12.0" [dependencies.unicode-width] version = "=0.1.12" [dependencies.vec_map] version = "0.8" [features] keymap-file = [] load_file = ["notify"] sapling-streampager-0.11.0/Cargo.toml.orig000064400000000000000000000020671046102023000165440ustar 00000000000000# @generated by autocargo from //eden/scm/lib/third-party/streampager:streampager [package] name = "sapling-streampager" version = "0.11.0" authors = ["Mark Juggurnauth-Thomas ", "Facebook Source Control Team "] edition = "2021" description = "streampager is a pager for command output or large files" license = "MIT" keywords = ["less", "more", "pager"] categories = ["command-line-utilities", "text-processing"] [lib] name = "streampager" [dependencies] bit-set = "0.5" dirs = "6.0" enum_dispatch = "0.3.13" indexmap = "2.2.6" lazy_static = "1.5" lru = { version = "=0.12.4", default-features = false } memmap2 = "0.5.10" notify = { version = "5", optional = true } regex = "1.11.1" scopeguard = "1.2.0" serde = { version = "1.0.185", features = ["derive"] } smallvec = { version = "1.13.2", default-features = false } tempfile = "3.15" terminfo = "0.9" termwiz = "0.23" thiserror = "2" toml = "0.8.19" unicode-segmentation = "1.12.0" unicode-width = "=0.1.12" vec_map = "0.8" [features] keymap-file = [] load_file = ["notify"] sapling-streampager-0.11.0/LICENSE.md000064400000000000000000000020701046102023000152530ustar 00000000000000MIT License Copyright (c) 2019 Mark Juggurnauth-Thomas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sapling-streampager-0.11.0/src/action.rs000064400000000000000000000137631046102023000162740ustar 00000000000000//! Actions. use std::sync::{Arc, Mutex}; use crate::error::Error; use crate::event::{Event, EventSender}; /// Actions that can be performed on the pager. #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum Action { /// Quit the pager. Quit, /// Refresh the screen. Refresh, /// Show the help screen. Help, /// Cancel the current action. Cancel, /// Switch to the previous file. PreviousFile, /// Switch to the next file. NextFile, /// Toggle visiblity of the ruler. ToggleRuler, /// Scroll up *n* lines. ScrollUpLines(usize), /// Scroll down *n* lines. ScrollDownLines(usize), /// Scroll up 1/*n* of the screen height. ScrollUpScreenFraction(usize), /// Scroll down 1/*n* of the screen height. ScrollDownScreenFraction(usize), /// Scroll to the top of the file. ScrollToTop, /// Scroll to the bottom of the file, and start following it. ScrollToBottom, /// Scroll left *n* columns. ScrollLeftColumns(usize), /// Scroll right *n* columns. ScrollRightColumns(usize), /// Scroll left 1/*n* of the screen width. ScrollLeftScreenFraction(usize), /// Scroll right 1/*n* of the screen width. ScrollRightScreenFraction(usize), /// Toggle display of line numbers. ToggleLineNumbers, /// Toggle line wrapping mode. ToggleLineWrapping, /// Prompt the user for a line to move to. PromptGoToLine, /// Prompt the user for a search term. The search will start at the beginning of the file. PromptSearchFromStart, /// Prompt the user for a search term. The search will start at the top of the screen. PromptSearchForwards, /// Prompt the user for a search term. The search will start from the bottom of the screen and /// proceed backwards. PromptSearchBackwards, /// Move to the previous match. PreviousMatch, /// Move to the next match. NextMatch, /// Move the previous line that contains a match. PreviousMatchLine, /// Move to the next line that contains a match. NextMatchLine, /// Move to the previous match, follow the current screen. PreviousMatchScreen, /// Move to the next match, follow the current screen. NextMatchScreen, /// Move to the first match. FirstMatch, /// Move to the last match. LastMatch, /// Append a digit to the "repeat count". /// The count defines how many times to do the next operation. AppendDigitToRepeatCount(usize), } impl std::fmt::Display for Action { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { use Action::*; match *self { Quit => write!(f, "Quit"), Refresh => write!(f, "Refresh the screen"), Help => write!(f, "Show this help"), Cancel => write!(f, "Close help or any open prompt"), PreviousFile => write!(f, "Switch to the previous file"), NextFile => write!(f, "Switch to the next file"), ToggleRuler => write!(f, "Toggle the ruler"), ScrollUpLines(1) => write!(f, "Scroll up"), ScrollUpLines(n) => write!(f, "Scroll up {} lines", n), ScrollDownLines(1) => write!(f, "Scroll down"), ScrollDownLines(n) => write!(f, "Scroll down {} lines", n), ScrollUpScreenFraction(1) => write!(f, "Scroll up one screen"), ScrollUpScreenFraction(n) => write!(f, "Scroll up 1/{} screen", n), ScrollDownScreenFraction(1) => write!(f, "Scroll down one screen"), ScrollDownScreenFraction(n) => write!(f, "Scroll down 1/{} screen", n), ScrollToTop => write!(f, "Move to the start of the file"), ScrollToBottom => write!(f, "Move to and follow the end of the file"), ScrollLeftColumns(1) => write!(f, "Scroll left"), ScrollLeftColumns(n) => write!(f, "Scroll left {} columns", n), ScrollRightColumns(1) => write!(f, "Scroll right"), ScrollRightColumns(n) => write!(f, "Scroll right {} columns", n), ScrollLeftScreenFraction(1) => write!(f, "Scroll left one screen"), ScrollLeftScreenFraction(n) => write!(f, "Scroll left 1/{} screen", n), ScrollRightScreenFraction(1) => write!(f, "Scroll right one screen"), ScrollRightScreenFraction(n) => write!(f, "Scroll right 1/{} screen", n), ToggleLineNumbers => write!(f, "Toggle line numbers"), ToggleLineWrapping => write!(f, "Cycle through line wrapping modes"), PromptGoToLine => write!(f, "Go to position in file"), PromptSearchFromStart => write!(f, "Search from the start of the file"), PromptSearchForwards => write!(f, "Search forwards"), PromptSearchBackwards => write!(f, "Search backwards"), PreviousMatch => write!(f, "Move to the previous match"), NextMatch => write!(f, "Move to the next match"), PreviousMatchLine => write!(f, "Move to the previous matching line"), NextMatchLine => write!(f, "Move the the next matching line"), PreviousMatchScreen => write!(f, "Move to the previous match following the screen"), NextMatchScreen => write!(f, "Move to the next match following the screen"), FirstMatch => write!(f, "Move to the first match"), LastMatch => write!(f, "Move to the last match"), AppendDigitToRepeatCount(n) => write!(f, "Append digit {} to repeat count", n), } } } /// A handle that can be used to send actions to the pager. #[derive(Clone)] pub struct ActionSender(Arc>); impl ActionSender { /// Create an action sender for an event sender. pub(crate) fn new(event_sender: EventSender) -> ActionSender { ActionSender(Arc::new(Mutex::new(event_sender))) } /// Send an action to the pager. pub fn send(&self, action: Action) -> Result<(), Error> { let sender = self.0.lock().unwrap(); sender.send(Event::Action(action))?; Ok(()) } } sapling-streampager-0.11.0/src/bar.rs000064400000000000000000000121641046102023000155550ustar 00000000000000//! A horizontal bar on the screen. use std::cmp::min; use std::sync::Arc; use termwiz::cell::CellAttributes; use termwiz::color::AnsiColor; use termwiz::surface::change::Change; use termwiz::surface::Position; use unicode_width::UnicodeWidthStr; use crate::util; /// A horizontal bar on the screen, e.g. the ruler or search bar. pub(crate) struct Bar { left_items: Vec>, right_items: Vec>, style: BarStyle, } /// An item in a bar. pub(crate) trait BarItem { fn width(&self) -> usize; fn render(&self, changes: &mut Vec, width: usize); } /// The style of the bar. This mostly affects the default background color. #[allow(unused)] #[derive(Clone, Copy, Debug)] pub(crate) enum BarStyle { // A normal bar with a silver background. Normal, // An informational bar with a teal background. Information, // A warning bar with a yellow background. Warning, // An error bar with a red background. Error, } impl BarStyle { fn background_color(self) -> AnsiColor { match self { BarStyle::Normal => AnsiColor::Silver, BarStyle::Information => AnsiColor::Teal, BarStyle::Warning => AnsiColor::Olive, BarStyle::Error => AnsiColor::Maroon, } } } impl Bar { pub(crate) fn new(style: BarStyle) -> Self { let left_items = Vec::new(); let right_items = Vec::new(); Bar { left_items, right_items, style, } } pub(crate) fn add_left_item(&mut self, item: Arc) { self.left_items.push(item); } pub(crate) fn add_right_item(&mut self, item: Arc) { self.right_items.push(item); } /// Render the bar to the given row on screen. pub(crate) fn render(&self, changes: &mut Vec, row: usize, width: usize) { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(row), }); let bar_attribs = CellAttributes::default() .set_foreground(AnsiColor::Black) .set_background(self.style.background_color()) .clone(); if width < 8 { // The area is too small to write anything useful, just write a blank bar. changes.push(Change::AllAttributes(bar_attribs)); changes.push(Change::ClearToEndOfLine( self.style.background_color().into(), )); return; } let padded_item_width = |item: &Arc| match item.width() { 0 => 0, w => w + 2, }; let mut left_items_width = self.left_items.iter().map(padded_item_width).sum(); let mut right_items_width = self.right_items.iter().map(padded_item_width).sum(); // The right-hand side is shown only if it can fit. if right_items_width + 2 > width { // Show only left items. right_items_width = 0; left_items_width = min(left_items_width, width.saturating_sub(2)); } else { // Show both items, truncating or padding the left items to the remaining width. left_items_width = width.saturating_sub(right_items_width + 2); } changes.push(Change::AllAttributes(bar_attribs.clone())); changes.push(Change::Text(String::from(" "))); let rendered_left_width = self.render_items( changes, self.left_items.as_slice(), left_items_width.saturating_sub(2), ); if right_items_width > 0 { changes.push(Change::AllAttributes(bar_attribs)); let gap = left_items_width.saturating_sub(rendered_left_width); changes.push(Change::Text(" ".repeat(gap))); self.render_items(changes, self.right_items.as_slice(), right_items_width); } changes.push(Change::ClearToEndOfLine( self.style.background_color().into(), )); } fn render_items( &self, changes: &mut Vec, items: &[Arc], width: usize, ) -> usize { let mut rendered_width = 0; for item in items.iter() { let item_width = item.width().min(width.saturating_sub(rendered_width)); if item_width > 0 { item.render(changes, item_width); rendered_width += item_width; let pad = min(width - rendered_width, 2); changes.push(Change::Text(" ".repeat(pad))); rendered_width += pad; if rendered_width >= width { break; } } } rendered_width } } pub(crate) struct BarString(String); impl BarString { pub(crate) fn new(s: impl Into) -> Self { BarString(s.into()) } } impl BarItem for BarString { fn width(&self) -> usize { self.0.as_str().width() } fn render(&self, changes: &mut Vec, width: usize) { changes.push(Change::Text(util::truncate_string( self.0.as_str(), 0, width, ))); } } sapling-streampager-0.11.0/src/bindings.rs000064400000000000000000000323301046102023000166030ustar 00000000000000//! Key bindings. use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use indexmap::IndexMap; use thiserror::Error; use crate::action::Action; use crate::file::FileIndex; /// Key codes for key bindings. /// pub use termwiz::input::KeyCode; /// Keyboard modifiers for key bindings. /// pub use termwiz::input::Modifiers; /// Errors specific to bindings. #[derive(Debug, Error)] pub enum BindingError { /// Error when a binding is invalid. #[error("invalid keybinding: {0}")] Invalid(String), /// Binding is missing a parameter. #[error("{0} missing parameter {1}")] MissingParameter(String, usize), /// Integer parsing error. #[error("invalid integer")] InvalidInt(#[from] std::num::ParseIntError), /// Wrapped error within the context of a binding parameter. #[error("invalid {binding} parameter {index}")] ForParameter { /// Wrapped error. #[source] error: Box, /// Binding. binding: String, /// Parameter index. index: usize, }, } impl BindingError { fn for_parameter(self, binding: String, index: usize) -> Self { Self::ForParameter { error: Box::new(self), binding, index, } } } type Result = std::result::Result; /// A key binding category. /// /// Key bindings are listed by category in the help screen. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Category { /// Uncategorized actions. None, /// Actions for controlling the pager. General, /// Actions for moving around the file. Navigation, /// Actions that affect the presentation of the file. Presentation, /// Actions that initiate or modify searches. Searching, /// Actions that are hidden in help view (for example, too verbose). Hidden, } impl Category { /// Non-hidden categories. pub(crate) fn categories() -> impl Iterator { [ Category::General, Category::Navigation, Category::Presentation, Category::Searching, Category::None, ] .iter() .cloned() } } impl std::fmt::Display for Category { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { Category::None => f.write_str("Other"), Category::General => f.write_str("General"), Category::Navigation => f.write_str("Navigation"), Category::Presentation => f.write_str("Presentation"), Category::Searching => f.write_str("Searching"), Category::Hidden => f.write_str("Hidden"), } } } /// An action that may be bound to a key. #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum Binding { /// An action. Action(Action), /// A custom binding. Custom(CustomBinding), /// An unrecognised binding. Unrecognized(String), } impl Binding { /// Create new custom binding. /// /// When this binding is invoked, the callback is called. The callback is provided with the /// file index of the file that is currently being displayed. Note that this may differ from /// any of the file indexes returned by the `add` methods on the `Pager`, as additional file /// indexes can be allocated, e.g. for the help screen. pub fn custom( category: Category, description: impl Into, callback: impl Fn(FileIndex) + Send + Sync + 'static, ) -> Self { Binding::Custom(CustomBinding::new(category, description, callback)) } pub(crate) fn category(&self) -> Category { match self { Binding::Action(action) => { use Action::*; match action { Quit | Refresh | Help | Cancel => Category::General, PreviousFile | NextFile | ScrollUpLines(_) | ScrollDownLines(_) | ScrollUpScreenFraction(_) | ScrollDownScreenFraction(_) | ScrollToTop | ScrollToBottom | ScrollLeftColumns(_) | ScrollRightColumns(_) | ScrollLeftScreenFraction(_) | ScrollRightScreenFraction(_) | PromptGoToLine => Category::Navigation, ToggleRuler | ToggleLineNumbers | ToggleLineWrapping => Category::Presentation, PromptSearchFromStart | PromptSearchForwards | PromptSearchBackwards | NextMatch | PreviousMatch | NextMatchLine | PreviousMatchLine | PreviousMatchScreen | NextMatchScreen | FirstMatch | LastMatch => Category::Searching, AppendDigitToRepeatCount(_) => Category::Hidden, } } Binding::Custom(binding) => binding.category, Binding::Unrecognized(_) => Category::None, } } /// Parse a keybinding identifier and list of parameters into a key binding. pub fn parse(ident: String, params: Vec) -> Result { use Action::*; let param_usize = |index| -> Result { let value: &String = params .get(index) .ok_or_else(|| BindingError::MissingParameter(ident.clone(), index))?; let value = value .parse::() .map_err(|err| BindingError::from(err).for_parameter(ident.clone(), index))?; Ok(value) }; let action = match ident.as_str() { "Quit" => Quit, "Refresh" => Refresh, "Help" => Help, "Cancel" => Cancel, "PreviousFile" => PreviousFile, "NextFile" => NextFile, "ToggleRuler" => ToggleRuler, "ScrollUpLines" => ScrollUpLines(param_usize(0)?), "ScrollDownLines" => ScrollDownLines(param_usize(0)?), "ScrollUpScreenFraction" => ScrollUpScreenFraction(param_usize(0)?), "ScrollDownScreenFraction" => ScrollDownScreenFraction(param_usize(0)?), "ScrollToTop" => ScrollToTop, "ScrollToBottom" => ScrollToBottom, "ScrollLeftColumns" => ScrollLeftColumns(param_usize(0)?), "ScrollRightColumns" => ScrollRightColumns(param_usize(0)?), "ScrollLeftScreenFraction" => ScrollLeftScreenFraction(param_usize(0)?), "ScrollRightScreenFraction" => ScrollRightScreenFraction(param_usize(0)?), "ToggleLineNumbers" => ToggleLineNumbers, "ToggleLineWrapping" => ToggleLineWrapping, "PromptGoToLine" => PromptGoToLine, "PromptSearchFromStart" => PromptSearchFromStart, "PromptSearchForwards" => PromptSearchForwards, "PromptSearchBackwards" => PromptSearchBackwards, "PreviousMatch" => PreviousMatch, "NextMatch" => NextMatch, "PreviousMatchLine" => PreviousMatchLine, "NextMatchLine" => NextMatchLine, "FirstMatch" => FirstMatch, "LastMatch" => LastMatch, _ => return Ok(Binding::Unrecognized(ident)), }; Ok(Binding::Action(action)) } } impl From for Binding { fn from(action: Action) -> Binding { Binding::Action(action) } } impl From for Option { fn from(action: Action) -> Option { Some(Binding::Action(action)) } } impl std::fmt::Display for Binding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { Binding::Action(ref a) => write!(f, "{}", a), Binding::Custom(ref b) => write!(f, "{}", b.description), Binding::Unrecognized(ref s) => write!(f, "Unrecognized binding ({})", s), } } } static CUSTOM_BINDING_ID: AtomicUsize = AtomicUsize::new(0); /// A custom binding. This can be used by applications using streampager /// to add custom actions on keys. #[derive(Clone)] pub struct CustomBinding { /// The id of this binding. This is unique for each binding. id: usize, /// The category of this binding. category: Category, /// The description of this binding. description: String, /// Called when the action is triggered. callback: Arc, } impl CustomBinding { /// Create a new custom binding. /// /// The category and description are used in the help screen. The /// callback is executed whenever the binding is triggered. pub fn new( category: Category, description: impl Into, callback: impl Fn(FileIndex) + Sync + Send + 'static, ) -> CustomBinding { CustomBinding { id: CUSTOM_BINDING_ID.fetch_add(1, Ordering::SeqCst), category, description: description.into(), callback: Arc::new(callback), } } /// Trigger the binding and run its callback. pub fn run(&self, file_index: FileIndex) { (self.callback)(file_index) } } impl PartialEq for CustomBinding { fn eq(&self, other: &Self) -> bool { self.id == other.id } } impl Eq for CustomBinding {} impl std::hash::Hash for CustomBinding { fn hash(&self, state: &mut H) { self.id.hash(state); } } impl std::fmt::Debug for CustomBinding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("CustomBinding") .field(&self.id) .field(&self.description) .finish() } } /// A binding to a key and its associated help visibility. Used by /// the keymaps macro to provide binding configuration. #[derive(Clone, Debug)] #[doc(hidden)] pub struct BindingConfig { /// The binding. pub binding: Binding, /// Whether this binding is visible in the help screen. pub visible: bool, } /// A collection of key bindings. #[derive(PartialEq, Eq)] pub struct Keymap { /// Map of bindings from keys. bindings: HashMap<(Modifiers, KeyCode), Binding>, /// Map of visible keys from bindings. keys: IndexMap>, } impl<'a, I: IntoIterator> From for Keymap { fn from(iter: I) -> Keymap { let iter = iter.into_iter(); let size_hint = iter.size_hint(); let mut bindings = HashMap::with_capacity(size_hint.0); let mut keys = IndexMap::with_capacity(size_hint.0); for &((modifiers, keycode), ref binding_config) in iter { bindings.insert((modifiers, keycode), binding_config.binding.clone()); if binding_config.visible { keys.entry(binding_config.binding.clone()) .or_insert_with(Vec::new) .push((modifiers, keycode)); } } Keymap { bindings, keys } } } impl Keymap { /// Create a new, empty, keymap. pub fn new() -> Self { Keymap { bindings: HashMap::new(), keys: IndexMap::new(), } } /// Get the binding associated with a key combination. pub fn get(&self, modifiers: Modifiers, keycode: KeyCode) -> Option<&Binding> { self.bindings.get(&(modifiers, keycode)) } /// Bind (or unbind) a key combination. pub fn bind( &mut self, modifiers: Modifiers, keycode: KeyCode, binding: impl Into>, ) -> &mut Self { self.bind_impl(modifiers, keycode, binding.into(), true) } /// Bind (or unbind) a key combination, but exclude it from the help screen. pub fn bind_hidden( &mut self, modifiers: Modifiers, keycode: KeyCode, binding: impl Into>, ) -> &mut Self { self.bind_impl(modifiers, keycode, binding.into(), false) } fn bind_impl( &mut self, modifiers: Modifiers, keycode: KeyCode, binding: Option, visible: bool, ) -> &mut Self { if let Some(old_binding) = self.bindings.remove(&(modifiers, keycode)) { if let Some(keys) = self.keys.get_mut(&old_binding) { keys.retain(|&item| item != (modifiers, keycode)); } } if let Some(binding) = binding { self.bindings.insert((modifiers, keycode), binding.clone()); if visible { self.keys .entry(binding) .or_insert_with(Vec::new) .push((modifiers, keycode)); } } self } pub(crate) fn iter_keys(&self) -> impl Iterator)> { self.keys.iter() } } impl Default for Keymap { fn default() -> Self { Keymap::from(crate::keymaps::default::KEYMAP.iter()) } } impl std::fmt::Debug for Keymap { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("Keymap") .field(&format!("<{} keys bound>", self.bindings.len())) .finish() } } sapling-streampager-0.11.0/src/buffer.rs000064400000000000000000000131331046102023000162570ustar 00000000000000//! Fillable buffer //! //! Buffers used for loading streams. use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Mutex, MutexGuard}; use std::usize; use memmap2::MmapMut; /// Fillable buffer /// /// This struct enapsulates a memory-mapped buffer that can be simulataneously written to and read /// from. Writes can only be appends, and reads can only happen to the portion that has already /// been written. pub(crate) struct Buffer { /// The underlying memory map. This can be accessed for both reading and writing, and so is /// stored in an UnsafeCell. _mmap: MmapMut, /// Pointer to the data in `_mmap`. data_ptr: *mut u8, /// The underlying memory map's capacity. capacity: usize, /// How much of the buffer has been filled. Reads are permitted in the range `0..filled`. /// Writes are permitted in the range `filled..`. filled: AtomicUsize, /// Lock for write access, to ensure only one writer can write at a time. lock: Mutex<()>, } unsafe impl Send for Buffer {} unsafe impl Sync for Buffer {} pub(crate) struct BufferWrite<'buffer> { /// The buffer this write is for. buffer: &'buffer Buffer, /// Lock guard for write access. _guard: MutexGuard<'buffer, ()>, } impl Buffer { pub(crate) fn new(capacity: usize) -> Buffer { let mut mmap = MmapMut::map_anon(capacity).unwrap(); let data_ptr = mmap.as_mut_ptr(); Buffer { _mmap: mmap, data_ptr, capacity, filled: AtomicUsize::new(0usize), lock: Mutex::new(()), } } /// Returns the writable portion of the buffer pub(crate) fn write(&self) -> BufferWrite<'_> { BufferWrite { buffer: self, _guard: self.lock.lock().unwrap(), } } /// Returns the readable portion of the buffer pub(crate) fn read(&self) -> &[u8] { let end = self.filled.load(Ordering::SeqCst); // Safety: `BufferWrite::written()` checks that `end <= capacity` unsafe { std::slice::from_raw_parts(self.data_ptr, end) } } #[cfg(feature = "load_file")] /// Returns the size of the readable portion of the buffer pub(crate) fn available(&self) -> usize { self.filled.load(Ordering::SeqCst) } } impl<'buffer> BufferWrite<'buffer> { /// Completes the write operation for `len` bytes to the buffer. After calling `written`, the /// data is made available to callers to `read`. pub(crate) fn written(self, len: usize) { let new_filled = self.buffer.filled.load(Ordering::SeqCst).saturating_add(len).clamp(0, isize::MAX as usize); assert!(new_filled <= self.buffer.capacity); self.buffer.filled.store(new_filled, Ordering::SeqCst); } } impl<'buffer> Deref for BufferWrite<'buffer> { type Target = [u8]; fn deref(&self) -> &[u8] { let start = self.buffer.filled.load(Ordering::SeqCst); let start_ptr = unsafe { self.buffer.data_ptr.add( start) }; // Safety: // * `BufferWrite::written()` enforces that `filled` is within capacity. It starts at 0 and // never shrinks. // * `_guard` enforces that no concurrent mutable reference exists // * The slices returned from `BufferWrite::deref()/deref_mut()` never overlap with those // from `Buffer::read` because the latter goes from `0..filled` and the former go from // `filled..capacity`. The latter are no longer accessible once `filled` has been updated // because `written()` consumes its argument. unsafe { std::slice::from_raw_parts(start_ptr, self.buffer.capacity - start) } } } impl<'buffer> DerefMut for BufferWrite<'buffer> { fn deref_mut(&mut self) -> &mut [u8] { let start = self.buffer.filled.load(Ordering::SeqCst); let start_ptr = unsafe { self.buffer.data_ptr.add( start) }; // Safety: // * `BufferWrite::written()` enforces that `filled` is within capacity. It starts at 0 and // never shrinks. // * `_guard` enforces that no concurrent mutable reference exists // * The slices returned from `BufferWrite::deref()/deref_mut()` never overlap with those // from `Buffer::read` because the latter goes from `0..filled` and the former go from // `filled..capacity`. The latter are no longer accessible once `filled` has been updated // because `written()` consumes its argument. unsafe { std::slice::from_raw_parts_mut(start_ptr, self.buffer.capacity - start) } } } #[cfg(test)] mod test { use super::*; use std::sync::Arc; use std::thread; #[test] fn test_buffer_write() { let b = Arc::new(Buffer::new(20)); let b2 = b.clone(); let b3 = b.clone(); let mut w = b.write(); // do a write w[0] = 42; w.written(1); // do some writes on other threads let t1 = thread::spawn(move || { let mut w = b2.write(); w[0] = 64; w.written(1); }); let t2 = thread::spawn(move || { let mut w = b3.write(); w[0] = 81; w.written(1); }); t1.join().unwrap(); t2.join().unwrap(); let mut w = b.write(); // do another write w[0] = 101; w[1] = 99; w.written(2); assert_eq!(b.read().len(), 5); assert_eq!(b.read()[0], 42); // these two writes could have happened in any order assert_eq!(b.read()[1] + b.read()[2], 64 + 81); assert_eq!(b.read()[3], 101); assert_eq!(b.read()[4], 99); } } sapling-streampager-0.11.0/src/command.rs000064400000000000000000000071171046102023000164310ustar 00000000000000//! Commands //! //! Commands the user can invoke. use crate::display::DisplayAction; use crate::error::Error; use crate::event::EventSender; use crate::file::FileInfo; use crate::prompt::Prompt; use crate::screen::Screen; use crate::search::{MatchMotion, Search, SearchKind}; /// Go to a line (Shortcut: ':') /// /// Prompts the user for a line number or percentage within the file and jumps /// to that position. Negative numbers can be used to refer to locations /// relative to the end of the file. pub(crate) fn goto() -> Prompt { Prompt::new( "goto", "Go to line:", Box::new( |screen: &mut Screen, value: &str| -> Result { match value { // Let vi users quit with `:q` muscle memory. "q" => return Ok(DisplayAction::Quit), "" => return Ok(DisplayAction::Render), _ => {} } let lines = screen.file.lines() as isize; if let Some(value_percent) = value.strip_suffix('%') { // Percentage match str::parse::(value_percent) { Ok(mut value_percent) => { value_percent = value_percent.max(-100).min(100); if value_percent < 0 { value_percent += 100; } let value = value_percent * (lines - 1) / 100; screen.scroll_to(value as usize); } Err(e) => { screen.error = Some(e.to_string()); } } } else { // Absolute match str::parse::(value) { Ok(value) => { let value = if value < -lines || value == 0 { 0 } else if value > lines { lines - 1 } else if value < 0 { lines + value - 1 } else { value - 1 }; screen.scroll_to(value as usize); } Err(e) => { screen.error = Some(e.to_string()); } } } Ok(DisplayAction::Render) }, ), ) } /// Search for text (Shortcuts: '/', '<', '>') /// /// Prompts the user for text to search. pub(crate) fn search(kind: SearchKind, event_sender: EventSender) -> Prompt { Prompt::new( "search", "Search:", Box::new( move |screen: &mut Screen, value: &str| -> Result { screen.refresh_matched_lines(); if value.is_empty() { match kind { SearchKind::First | SearchKind::FirstAfter(_) => { screen.move_match(MatchMotion::NextLine) } SearchKind::FirstBefore(_) => screen.move_match(MatchMotion::PreviousLine), } } else { screen.set_search( Search::new(&screen.file, value, kind, event_sender.clone()).ok(), ); } Ok(DisplayAction::Render) }, ), ) } sapling-streampager-0.11.0/src/config.rs000064400000000000000000000167001046102023000162560ustar 00000000000000//! Configuration that affects Pager behaviors. use std::sync::Arc; use std::time::Duration; use serde::Deserialize; use crate::bindings::Keymap; use crate::error::Result; /// Specify what interface to use. #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] #[serde(from = "&str")] pub enum InterfaceMode { /// The full screen terminal interface. /// /// Support text search and other operations. /// /// Use the alternate screen. The pager UI will disappear completely at /// exit (except for terminals without alternate screen support). /// /// Similar to external command `less` without flags. This is the default. FullScreen, /// The minimal interface. Output goes to the terminal directly. /// /// Does not support text search or other fancy operations. /// /// Does not use the alternate screen. Content will be kept in the terminal /// at exit. /// /// Error messages and progress messages are printed after /// outputs. /// /// Similar to shell command `cat` without buffering. Direct, /// Hybrid: `Direct` first, `FullScreen` next. /// /// `Direct` is used initially. When content exceeds one screen, switch to the /// `FullScreen` interface. /// /// Unlike `FullScreen` or `Delayed`, skip initializing the alternate /// screen. This is because the initial `Direct` might have "polluted" /// the terminal. /// /// Similar to external command `less -F -X`. Hybrid, /// Wait to decide. /// /// If output completes in the delayed time, and is within one screen, print /// the output and exit. Otherwise, enter the `FullScreen` interface. /// /// Unlike `Hybrid`, output is buffered in memory. So the terminal is not /// "polluted" and the alternate screen is used for the `FullScreen` /// interface. /// /// If duration is set to infinite, similar to external command `less -F`. /// If duration is set to 0, similar to `FullScreen`. Delayed(Duration), } impl Default for InterfaceMode { fn default() -> Self { Self::FullScreen } } impl From<&str> for InterfaceMode { fn from(value: &str) -> InterfaceMode { match value.to_lowercase().as_ref() { "full" | "fullscreen" | "" => InterfaceMode::FullScreen, "direct" => InterfaceMode::Direct, "hybrid" => InterfaceMode::Hybrid, s if s.starts_with("delayed") => { let duration = s.rsplit(':').next().unwrap_or("inf"); let duration = if duration.ends_with("ms") { // ex. delayed:100ms Duration::from_millis(duration.trim_end_matches("ms").parse().unwrap_or(0)) } else { // ex. delayed:1s, delayed:1, delayed Duration::from_secs(duration.trim_end_matches('s').parse().unwrap_or(1 << 30)) }; InterfaceMode::Delayed(duration) } _ => InterfaceMode::default(), } } } /// Specify the default line wrapping mode. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)] pub enum WrappingMode { /// Lines are not wrapped. #[serde(rename = "none")] Unwrapped, /// Lines are wrapped on grapheme boundaries. #[serde(rename = "line")] GraphemeBoundary, /// Lines are wrapped on word boundaries. #[serde(rename = "word")] WordBoundary, } impl WrappingMode { pub(crate) fn next_mode(self) -> WrappingMode { match self { WrappingMode::Unwrapped => WrappingMode::GraphemeBoundary, WrappingMode::GraphemeBoundary => WrappingMode::WordBoundary, WrappingMode::WordBoundary => WrappingMode::Unwrapped, } } } impl Default for WrappingMode { fn default() -> Self { Self::Unwrapped } } /// Keymap Configuration #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(from = "&str")] pub enum KeymapConfig { /// A keymap name to be loaded. Name(String), /// An already-loaded keymap. Keymap(Arc), } impl KeymapConfig { pub(crate) fn load(&self) -> Result> { match self { Self::Name(name) => Ok(Arc::new(crate::keymaps::load(name)?)), Self::Keymap(keymap) => Ok(keymap.clone()), } } } impl Default for KeymapConfig { fn default() -> Self { Self::Name(String::from("default")) } } impl From<&str> for KeymapConfig { fn from(value: &str) -> Self { Self::Name(String::from(value)) } } /// A group of configurations. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(default)] pub struct Config { /// Specify when to use fullscreen. pub interface_mode: InterfaceMode, /// Specify whether scrolling down can past end of file. pub scroll_past_eof: bool, /// Specify how many lines to read ahead. pub read_ahead_lines: usize, /// Specify whether to poll input during start-up (delayed or direct mode). pub startup_poll_input: bool, /// Specify whether to show the ruler by default. pub show_ruler: bool, /// Specify whether to show the cursor by default. pub show_cursor: bool, /// Specify default wrapping move. pub wrapping_mode: WrappingMode, /// Specify the name of the default key map. pub keymap: KeymapConfig, } impl Default for Config { fn default() -> Self { Self { interface_mode: Default::default(), scroll_past_eof: true, read_ahead_lines: crate::file::DEFAULT_NEEDED_LINES, startup_poll_input: false, show_ruler: true, // See issue #52. With cursor hidden, scrolling is flaky in VSCode terminal. show_cursor: std::env::var("TERM_PROGRAM").ok().as_deref() == Some("vscode"), wrapping_mode: Default::default(), keymap: Default::default(), } } } impl Config { /// Create [`Config`] from the user's default config file. pub fn from_config_file() -> Self { if let Some(mut path) = dirs::config_dir() { path.push("streampager"); path.push("streampager.toml"); if let Ok(config) = std::fs::read_to_string(&path) { match toml::from_str(&config) { Ok(config) => return config, Err(e) => eprintln!( "streampager: failed to parse config at {:?}, using defaults: {}", path, e ), } } } Self::default() } /// Modify [`Config`] using environment variables. pub fn with_env(mut self) -> Self { use std::env::var; if let Ok(s) = var("SP_INTERFACE_MODE") { self.interface_mode = InterfaceMode::from(s.as_ref()); } if let Ok(s) = var("SP_SCROLL_PAST_EOF") { if let Some(b) = parse_bool(&s) { self.scroll_past_eof = b; } } if let Ok(s) = var("SP_READ_AHEAD_LINES") { if let Ok(n) = s.parse::() { self.read_ahead_lines = n; } } self } pub(crate) fn from_user_config() -> Self { Self::from_config_file().with_env() } } fn parse_bool(value: &str) -> Option { match value.to_ascii_lowercase().as_ref() { "1" | "yes" | "true" | "on" | "always" => Some(true), "0" | "no" | "false" | "off" | "never" => Some(false), _ => None, } } sapling-streampager-0.11.0/src/control.rs000064400000000000000000000216721046102023000164750ustar 00000000000000//! Controlled files. //! //! Files where data is provided by a controller. use std::borrow::Cow; use std::ops::Range; use std::sync::{Arc, Mutex, RwLock}; use thiserror::Error; use crate::event::{Event, EventSender}; use crate::file::{FileIndex, FileInfo}; /// Errors that may occur during controlled file operations. #[derive(Debug, Error)] pub enum ControlledFileError { /// Line number out of range. #[error("line number {index} out of range (0..{length})")] LineOutOfRange { /// The index of the line number that is out of range. index: usize, /// The length of the file (and so the limit for the line number). length: usize, }, /// Other error type. #[error(transparent)] Error(#[from] crate::error::Error), } /// Result alias for controlled file operations that may fail. pub type Result = std::result::Result; /// A controller for a controlled file. /// /// This contains a logical file which can be mutated by a controlling /// program. It can be added to the pager using /// `Pager::add_controlled_file`. #[derive(Clone)] pub struct Controller { data: Arc>, notify: Arc>>, } impl Controller { /// Create a new controller. The controlled file is initially empty. pub fn new(title: impl Into) -> Controller { Controller { data: Arc::new(RwLock::new(FileData::new(title))), notify: Arc::new(Mutex::new(Vec::new())), } } /// Returns a copy of the current title. pub fn title(&self) -> String { let data = self.data.read().unwrap(); data.title.clone() } /// Returns a copy of the current file info. pub fn info(&self) -> String { let data = self.data.read().unwrap(); data.info.clone() } /// Apply a sequence of changes to the controlled file. pub fn apply_changes(&self, changes: impl IntoIterator) -> Result<()> { let mut data = self.data.write().unwrap(); for change in changes { data.apply_change(change)?; } // TODO(markbt): more fine-grained notifications. // For now, just reload the file. let notify = self.notify.lock().unwrap(); for (event_sender, index) in notify.iter() { event_sender.send(Event::Reloading(*index))?; } Ok(()) } } /// A change to apply to a controlled file. pub enum Change { /// Set the title for the file. SetTitle { /// The new title. title: String, }, /// Set the file information for the file. SetInfo { /// The text of the new file info. info: String, }, /// Append a single line to the file. AppendLine { /// The content of the new line. content: Vec, }, /// Insert a single line into the file. InsertLine { /// Index of the line in the file to insert before. before_index: usize, /// The content of the new line. content: Vec, }, /// Replace a single line in the file. ReplaceLine { /// Index of the line in fhe file to replace. index: usize, /// The content of the new line. content: Vec, }, /// Delete a single line from the file. DeleteLine { /// Index of the line in the file to delete. index: usize, }, /// Append multiple lines to the file AppendLines { /// The contents of the new lines. contents: Vec>, }, /// Insert some lines before another line in the file. InsertLines { /// Index of the line in the file to insert before. before_index: usize, /// The contents of the new lines. contents: Vec>, }, /// Replace a range of lines with another set of lines. /// The range and the new lines do not need to be the same size. ReplaceLines { /// The range of lines in the file to replace. range: Range, /// The contents of the new lines. contents: Vec>, }, /// Delete a range of lines in the file. DeleteLines { /// The range of lines in the file to delete. range: Range, }, /// Replace all lines with another set of lines. ReplaceAll { /// The new contents of the file. contents: Vec>, }, } /// A file whose contents is controlled by a `Controller`. #[derive(Clone)] pub struct ControlledFile { index: FileIndex, data: Arc>, } impl ControlledFile { pub(crate) fn new( controller: &Controller, index: FileIndex, event_sender: EventSender, ) -> ControlledFile { let mut notify = controller.notify.lock().unwrap(); notify.push((event_sender, index)); ControlledFile { index, data: controller.data.clone(), } } } impl FileInfo for ControlledFile { /// The file's index. fn index(&self) -> FileIndex { self.index } /// The file's title. fn title(&self) -> Cow<'_, str> { let data = self.data.read().unwrap(); Cow::Owned(data.title.clone()) } /// The file's info. fn info(&self) -> Cow<'_, str> { let data = self.data.read().unwrap(); Cow::Owned(data.info.clone()) } /// True once the file is loaded and all newlines have been parsed. fn loaded(&self) -> bool { true } /// Returns the number of lines in the file. fn lines(&self) -> usize { self.data.read().unwrap().lines.len() } /// Runs the `call` function, passing it the contents of line `index`. /// Tries to avoid copying the data if possible, however the borrowed /// line only lasts as long as the function call. fn with_line(&self, index: usize, mut call: F) -> Option where F: FnMut(Cow<'_, [u8]>) -> T, { let data = self.data.read().unwrap(); data.lines.get(index).map(|line| call(Cow::Borrowed(line.content.as_slice()))) } /// Set how many lines are needed. /// /// If `self.lines()` exceeds that number, pause loading until /// `set_needed_lines` is called with a larger number. /// This is only effective for "streamed" input. fn set_needed_lines(&self, _lines: usize) {} /// True if the loading thread has been paused. fn paused(&self) -> bool { false } } struct FileData { title: String, info: String, lines: Vec, } impl FileData { fn new(title: impl Into) -> FileData { FileData { title: title.into(), info: String::new(), lines: Vec::new(), } } fn line_mut(&mut self, index: usize) -> Result<&mut LineData> { let length = self.lines.len(); if let Some(line) = self.lines.get_mut(index) { return Ok(line); } Err(ControlledFileError::LineOutOfRange { index, length }) } fn apply_change(&mut self, change: Change) -> Result<()> { match change { Change::SetTitle { title } => { self.title = title; } Change::SetInfo { info } => { self.info = info; } Change::AppendLine { content } => { self.lines.push(LineData::with_content(content)); } Change::InsertLine { before_index, content, } => { self.lines .insert(before_index, LineData::with_content(content)); } Change::ReplaceLine { index, content } => { self.line_mut(index)?.content = content; } Change::DeleteLine { index } => { self.lines.remove(index); } Change::AppendLines { contents } => { let new_lines = contents.into_iter().map(LineData::with_content); self.lines.extend(new_lines); } Change::InsertLines { before_index, contents, } => { let new_lines = contents.into_iter().map(LineData::with_content); self.lines.splice(before_index..before_index, new_lines); } Change::ReplaceLines { range, contents } => { let new_lines = contents.into_iter().map(LineData::with_content); self.lines.splice(range, new_lines); } Change::DeleteLines { range } => { self.lines.splice(range, std::iter::empty()); } Change::ReplaceAll { contents } => { let new_lines = contents.into_iter().map(LineData::with_content); self.lines = new_lines.collect(); } } Ok(()) } } struct LineData { content: Vec, } impl LineData { fn with_content(content: Vec) -> LineData { LineData { content } } } sapling-streampager-0.11.0/src/direct.rs000064400000000000000000000305731046102023000162670ustar 00000000000000//! Support for `InterfaceMode::Direct` and other modes using `Direct`. use std::time::Duration; use std::time::Instant; use bit_set::BitSet; use termwiz::input::InputEvent; use termwiz::surface::change::Change; use termwiz::surface::CursorVisibility; use termwiz::surface::Position; use termwiz::terminal::Terminal; use vec_map::VecMap; use crate::action::Action; use crate::config::InterfaceMode; use crate::config::WrappingMode; use crate::error::Error; use crate::error::Result; use crate::event::Event; use crate::event::EventStream; use crate::file::File; use crate::file::FileInfo; use crate::line::Line; use crate::progress::Progress; /// Return value of `direct`. #[derive(Debug)] pub(crate) enum Outcome { /// Content is not completely rendered. A hint to enter full-screen. /// The number of rows that have been rendered is included. RenderIncomplete(usize), /// Content is not rendered at all. A hint to enter full-screen. RenderNothing, /// Content is completely rendered. RenderComplete, /// The user pressed a key to exit. Interrupted, } /// Streaming content to the terminal without entering full screen. /// /// Similar to `tail -f`, but with dynamic progress support. /// Useful for rendering content before entering the full-screen mode. /// /// Lines are rendered in this order: /// - Output (append-only) /// - Error (append-only) /// - Progress (mutable) /// /// Return `Outcome::Interrupted` if `q` or `Ctrl+C` is pressed. /// Otherwise, return values and conditions are as follows: /// /// | Interface | Fits Screen | Streams Ended | Return | /// |------------|-------------|---------------|------------------| /// | FullScreen | (any) | (any) | RenderNothing | /// | Direct | (any) | no | - | /// | Direct | (any) | yes | RenderComplete | /// | Hybrid | yes | no | - | /// | Hybrid | yes | yes | RenderComplete | /// | Hybrid | no | (any) | RenderIncomplete | /// | Delayed | (any) | no (time out) | RenderNothing | /// | Delayed | yes | yes | RenderComplete | /// | Delayed | no | yes | RenderNothing | pub(crate) fn direct( term: &mut T, output_files: &[File], error_files: &[File], progress: Option<&Progress>, events: &mut EventStream, mode: InterfaceMode, poll_input: bool, ) -> Result { if mode == InterfaceMode::FullScreen { return Ok(Outcome::RenderNothing); } let delayed_deadline = match mode { InterfaceMode::Delayed(duration) => Some(Instant::now() + duration), _ => None, }; let mut loading = BitSet::with_capacity(output_files.len() + error_files.len()); for file in output_files.iter().chain(error_files.iter()) { loading.insert(file.index()); } let mut last_read = VecMap::new(); // file index -> line number last read let mut collect_unread = |files: &[File], max_lines: usize| -> Vec> { let mut result = Vec::new(); for file in files.iter() { let index = file.index(); let mut lines = file.lines(); let last = last_read.get(index).cloned().unwrap_or(0); file.set_needed_lines(last + max_lines); // Ignore the incomplete last line if the file is loading. if lines > 0 && !file.loaded() && file .with_line(lines - 1, |l| !l.ends_with(b"\n")) .unwrap_or(true) { lines -= 1; } if lines >= last { let lines = (last + max_lines).min(lines); result.reserve(lines - last); for i in last..lines { file.with_line(i, |l| result.push(l.to_vec())); } last_read.insert(index, lines); } } result }; let read_progress_lines = || -> Vec> { let line_count = progress.map_or(0, |p| p.lines()); (0..line_count) .filter_map(|i| progress.and_then(|p| p.with_line(i, |l| l.to_vec()))) .collect::>() }; let mut state = StreamingLines::default(); let delayed = delayed_deadline.is_some(); let has_one_screen_limit = !matches!(mode, InterfaceMode::Direct); let mut render = |term: &mut T, h: usize, w: usize| -> Result> { let append_output_lines = collect_unread(output_files, h + 2); let append_error_lines = collect_unread(error_files, h + 2); let progress_lines = read_progress_lines(); state.add_lines(append_output_lines, append_error_lines, progress_lines); if delayed { if has_one_screen_limit && state.height(w) >= h { return Ok(Some(Outcome::RenderNothing)); } } else { if has_one_screen_limit && state.height(w) >= h { return Ok(Some(Outcome::RenderIncomplete(state.rendered_row_count()))); } let changes = state.render_pending_lines(w)?; term.render(&changes).map_err(Error::Termwiz)?; } Ok(None) }; let mut size = term.get_screen_size().map_err(Error::Termwiz)?; let mut loaded = BitSet::with_capacity(loading.capacity()); let mut remaining = output_files.len() + error_files.len(); let interval = Duration::from_millis(10); while remaining > 0 { let maybe_event = if poll_input { events.get(term, Some(interval))? } else { events.try_recv(Some(interval))? }; match maybe_event { Some(Event::Loaded(i)) => { if loading.contains(i) && loaded.insert(i) { remaining -= 1; } } Some(Event::Input(InputEvent::Resized { .. })) => { size = term.get_screen_size().map_err(Error::Termwiz)?; } Some(Event::Input(InputEvent::Key(key))) => { use termwiz::input::KeyCode::Char; use termwiz::input::Modifiers; match (key.modifiers, key.key) { (Modifiers::NONE, Char('q')) | (Modifiers::CTRL, Char('c')) => { term.render(&state.abort()).map_err(Error::Termwiz)?; return Ok(Outcome::Interrupted); } (Modifiers::NONE, Char('f')) | (Modifiers::NONE, Char(' ')) => { let outcome = if delayed { Outcome::RenderNothing } else { Outcome::RenderIncomplete(state.rendered_row_count()) }; return Ok(outcome); } _ => (), } } Some(Event::Action(Action::Quit)) => { term.render(&state.abort()).map_err(Error::Termwiz)?; return Ok(Outcome::Interrupted); } _ => (), } if let Some(deadline) = delayed_deadline { if deadline <= Instant::now() { return Ok(Outcome::RenderNothing); } } if let Some(outcome) = render(term, size.rows, size.cols)? { return Ok(outcome); } } if delayed { term.render(&state.render_pending_lines(size.cols)?) .map_err(Error::Termwiz)?; } Ok(Outcome::RenderComplete) } /// State for calculating how to incrementally render streaming changes. /// /// +----------------------------+ /// | past output (never redraw) | /// +----------------------------+ /// | new output (just received) | /// +----------------------------+ /// | error (always redraw) | /// +----------------------------+ /// | progress (always redraw) | /// +----------------------------+ #[derive(Default)] struct StreamingLines { past_output_row_count: usize, new_output_lines: Vec>, error_lines: Vec>, progress_lines: Vec>, erase_row_count: usize, pending_changes: bool, cursor_hidden: bool, } impl StreamingLines { fn add_lines( &mut self, mut append_output_lines: Vec>, mut append_error_lines: Vec>, replace_progress_lines: Vec>, ) { if append_output_lines.is_empty() && append_error_lines.is_empty() && replace_progress_lines == self.progress_lines { return; } self.new_output_lines.append(&mut append_output_lines); self.error_lines.append(&mut append_error_lines); self.progress_lines = replace_progress_lines; self.pending_changes = true; } fn render_pending_lines(&mut self, terminal_width: usize) -> Result> { // Fast path: nothing changed? if !self.pending_changes { return Ok(Vec::new()); } // Every line needs at least 2 `Change`s: Text, and CursorPosition, // plus 2 Changes for erasing existing lines. let line_count = self.new_output_lines.len() + self.error_lines.len() + self.progress_lines.len(); let mut changes = Vec::with_capacity(line_count * 2 + 2); // Step 1: Erase progress, and error. if self.erase_row_count > 0 { let dy = -(self.erase_row_count as isize); changes.push(Change::CursorPosition { x: Position::Relative(0), y: Position::Relative(dy), }); changes.push(Change::ClearToEndOfScreen(Default::default())); } // Step 2: Render new output + error + progress let mut render = |lines| -> Result<_> { let mut row_count = 0; for line in lines { let line = Line::new(0, line); let height = line.height(terminal_width, WrappingMode::GraphemeBoundary); line.render(&mut changes, 0, terminal_width * height, None); changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Relative(1), }); row_count += height; } Ok(row_count) }; let new_output_row_count = render(self.new_output_lines.iter())?; let error_row_count = render(self.error_lines.iter())?; let mut progress_row_count = render(self.progress_lines.iter())?; // Don't render the last newline after progress, and hide the // cursor while progress is being shown. if progress_row_count > 0 { changes.pop(); changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Relative(0), }); if !self.cursor_hidden { changes.push(Change::CursorVisibility(CursorVisibility::Hidden)); self.cursor_hidden = true; } progress_row_count -= 1; } else if self.cursor_hidden { changes.push(Change::CursorVisibility(CursorVisibility::Visible)); self.cursor_hidden = false; } // Step 3: Update internal state. self.past_output_row_count += new_output_row_count; self.new_output_lines.clear(); self.erase_row_count = error_row_count + progress_row_count; self.pending_changes = false; Ok(changes) } fn abort(&mut self) -> Vec { let mut changes = Vec::new(); if self.cursor_hidden { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Relative(1), }); changes.push(Change::CursorVisibility(CursorVisibility::Visible)); self.cursor_hidden = false; } changes } fn height(&self, terminal_width: usize) -> usize { let mut row_count = self.past_output_row_count; for line in self .new_output_lines .iter() .chain(self.error_lines.iter()) .chain(self.progress_lines.iter()) { let line = Line::new(0, line); row_count += line.height(terminal_width, WrappingMode::GraphemeBoundary); } row_count } fn rendered_row_count(&self) -> usize { self.past_output_row_count + self.erase_row_count } } sapling-streampager-0.11.0/src/display.rs000064400000000000000000000401231046102023000164520ustar 00000000000000//! Manage the Display. use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; use scopeguard::guard; use termwiz::caps::Capabilities as TermCapabilities; use termwiz::cell::CellAttributes; use termwiz::color::ColorAttribute; use termwiz::input::InputEvent; use termwiz::surface::change::Change; use termwiz::surface::{CursorVisibility, Position}; use termwiz::terminal::Terminal; use vec_map::VecMap; use crate::command; use crate::config::Config; use crate::direct; use crate::error::Error; use crate::event::{Event, EventStream, UniqueInstance}; use crate::file::{File, FileIndex, FileInfo, LoadedFile}; use crate::help::help_text; use crate::progress::Progress; use crate::screen::Screen; use crate::search::SearchKind; /// Capabilities of the terminal that we care about. #[derive(Default)] pub(crate) struct Capabilities { pub(crate) scroll_up: bool, pub(crate) scroll_down: bool, } impl Capabilities { fn new(term_caps: TermCapabilities) -> Capabilities { use terminfo::capability as cap; let mut caps = Capabilities::default(); if let Some(db) = term_caps.terminfo_db() { if db.get::().is_some() { caps.scroll_up = db.get::().is_some() || (db.get::().is_some() && db.get::().is_some()); caps.scroll_down = db.get::().is_some() || (db.get::().is_some() && db.get::().is_some()); } } caps } } /// An action that affects the display. pub(crate) enum DisplayAction { /// Do nothing. None, /// Run a function. The function may return a new action to run next. Run(Box Result>), /// Change the terminal. Change(Change), /// Render the parts of the screen that have changed. Render, /// Render the whole screen. Refresh, /// Render the prompt. RefreshPrompt, /// Move to the next file. NextFile, /// Move to the previous file. PreviousFile, /// Show the help screen. ShowHelp, /// Clear the overlay. ClearOverlay, /// Close the program. Quit, } /// Container for all screens. struct Screens { /// The loaded files. screens: Vec, /// An overlaid screen (e.g. the help screen). overlay: Option, /// The currently active screen. current_index: FileIndex, /// The file index of the overlay. While overlays aren't part of the /// screens vector, we still need a file index so that the file loader can /// report loading completion and the search thread can report search /// matches. Use an index starting after the loaded files for this purpose. /// Each time a new overlay is added, this index is incremented, so that /// each overlay gets a unique index. overlay_index: FileIndex, } impl Screens { /// Create a new screens container for the given files. fn new( files: Vec, mut error_files: VecMap, progress: Option, config: Arc, ) -> Result { let count = files.len(); let mut screens = Vec::new(); for file in files.into_iter() { let index = file.index(); let mut screen = Screen::new(file, config.clone())?; screen.set_progress(progress.clone()); screen.set_error_file(error_files.remove(index)); screens.push(screen); } Ok(Screens { screens, overlay: None, current_index: 0, overlay_index: count, }) } /// Get the current screen. fn current(&mut self) -> &mut Screen { if let Some(ref mut screen) = self.overlay { screen } else { &mut self.screens[self.current_index] } } /// True if the given index is the index of the currently visible screen. fn is_current_index(&self, index: FileIndex) -> bool { match self.overlay { Some(_) => index == self.overlay_index, None => index == self.current_index, } } /// Get the screen with the given index. fn get(&mut self, index: usize) -> Option<&mut Screen> { if index == self.overlay_index { self.overlay.as_mut() } else if index < self.screens.len() { Some(&mut self.screens[index]) } else { None } } } /// Start displaying files. pub(crate) fn start( mut term: impl Terminal, term_caps: TermCapabilities, mut events: EventStream, files: Vec, error_files: VecMap, progress: Option, config: Config, ) -> Result<(), Error> { // Defer enabling raw mode until we need it. It has some undesirable side // effects for direct mode such as disabling terminal echo and consuming all // pending terminal input (e.g. user types next terminal command before the // current command has finished). let mut in_raw_mode = false; if config.startup_poll_input { // We need raw mode to poll for user input during direct mode. term.set_raw_mode().map_err(Error::Termwiz)?; in_raw_mode = true; } let outcome = { // Only take the first output and error. This emulates the behavior that // the main pager can only display one stream at a time. let output_files = &files[0..1.min(files.len())]; let error_files = match error_files.iter().next() { None => Vec::new(), Some((_i, file)) => vec![file.clone()], }; direct::direct( &mut term, output_files, &error_files[..], progress.as_ref(), &mut events, config.interface_mode, config.startup_poll_input, )? }; match outcome { direct::Outcome::RenderComplete | direct::Outcome::Interrupted => return Ok(()), direct::Outcome::RenderIncomplete(rows) => { // Push the rendered output up to the top of the screen, so that // when we start rendering full screen we don't overwrite output // from earlier commands. In direct mode the bottom line held the // cursor, so we must subtract that line, too, otherwise we will // scroll up too far. let size = term.get_screen_size().map_err(Error::Termwiz)?; let scroll_count = size.rows.saturating_sub(rows).saturating_sub(1); if scroll_count > 0 { term.render(&[Change::Text("\n".repeat(scroll_count))]) .map_err(Error::Termwiz)?; } } direct::Outcome::RenderNothing => term.enter_alternate_screen().map_err(Error::Termwiz)?, }; // We certainly need raw mode for fullscreen. if !in_raw_mode { term.set_raw_mode().map_err(Error::Termwiz)?; } let overlay_height = AtomicUsize::new(0); let mut term = guard(term, |mut term| { // Clean up when exiting. Most of this should be achieved by exiting // the alternate screen, but just in case it isn't, move to the // bottom of the screen and reset all attributes. let size = term.get_screen_size().unwrap(); let overlay_height = overlay_height.load(Ordering::SeqCst); let scroll_count = 1usize.saturating_sub(overlay_height); term.render(&[ Change::CursorVisibility(CursorVisibility::Visible), Change::AllAttributes(CellAttributes::default()), Change::ScrollRegionUp { first_row: 0, region_size: size.rows, scroll_count, }, Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(size.rows.saturating_sub(overlay_height + scroll_count)), }, Change::ClearToEndOfScreen(ColorAttribute::default()), ]) .unwrap(); }); let config = Arc::new(config); let caps = Capabilities::new(term_caps); let mut screens = Screens::new(files, error_files, progress, config.clone())?; let event_sender = events.sender(); let render_unique = UniqueInstance::new(); let refresh_unique = UniqueInstance::new(); { let screen = screens.current(); let size = term.get_screen_size().map_err(Error::Termwiz)?; screen.resize(size.cols, size.rows); screen.maybe_load_more(); term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; } loop { // Listen for an event or input. If we are animating, put a timeout on the wait. let timeout = if screens.current().animate() { Some(Duration::from_millis(100)) } else { None }; let event = events.get(&mut *term, timeout)?; // Dispatch the event and receive an action to take. let mut action = { let screen = screens.current(); screen.maybe_load_more(); match event { None => screen.dispatch_animation(), Some(Event::Render) => { term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; DisplayAction::None } Some(Event::Input(InputEvent::Resized { .. })) => { let size = term.get_screen_size().map_err(Error::Termwiz)?; screen.resize(size.cols, size.rows); term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; DisplayAction::None } Some(Event::Refresh) => { let size = term.get_screen_size().map_err(Error::Termwiz)?; screen.resize(size.cols, size.rows); screen.refresh(); term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; DisplayAction::None } Some(Event::Progress) => { screen.refresh_progress(); term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; DisplayAction::None } Some(Event::Action(action)) => screen.dispatch_action(action, &event_sender), Some(Event::Input(InputEvent::Key(key))) => { let width = screen.width(); if let Some(prompt) = screen.prompt() { prompt.dispatch_key(key, width) } else { screen.dispatch_key(key, &event_sender) } } Some(Event::Input(InputEvent::Paste(ref text))) => { let width = screen.width(); screen .prompt() .get_or_insert_with(|| { // Assume the user wanted to search for what they're pasting. command::search(SearchKind::First, event_sender.clone()) }) .paste(text, width) } Some(Event::Loaded(index)) if screens.is_current_index(index) => { DisplayAction::Refresh } #[cfg(feature = "load_file")] Some(Event::Appending(index)) if screens.is_current_index(index) => { DisplayAction::Refresh } Some(Event::Reloading(index)) => { if let Some(screen) = screens.get(index) { screen.flush_line_caches(); } if screens.is_current_index(index) { DisplayAction::Refresh } else { DisplayAction::None } } Some(Event::SearchFirstMatch(index)) => { if let Some(screen) = screens.get(index) { screen.search_first_match() } else { DisplayAction::None } } Some(Event::SearchFinished(index)) => { if let Some(screen) = screens.get(index) { screen.search_finished() } else { DisplayAction::None } } _ => DisplayAction::None, } }; // Process the action. We may get new actions in return from the action. loop { match std::mem::replace(&mut action, DisplayAction::None) { DisplayAction::None => break, DisplayAction::Run(mut f) => action = f(screens.current())?, DisplayAction::Change(c) => { term.render(&[c]).map_err(Error::Termwiz)?; } DisplayAction::Render => event_sender.send_unique(Event::Render, &render_unique)?, DisplayAction::Refresh => { event_sender.send_unique(Event::Refresh, &refresh_unique)? } DisplayAction::RefreshPrompt => { screens.current().refresh_prompt(); event_sender.send_unique(Event::Render, &render_unique)?; } DisplayAction::NextFile => { screens.overlay = None; if screens.current_index < screens.screens.len() - 1 { screens.current_index += 1; let screen = screens.current(); let size = term.get_screen_size().map_err(Error::Termwiz)?; screen.resize(size.cols, size.rows); screen.refresh(); term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; } } DisplayAction::PreviousFile => { screens.overlay = None; if screens.current_index > 0 { screens.current_index -= 1; let screen = screens.current(); let size = term.get_screen_size().map_err(Error::Termwiz)?; screen.resize(size.cols, size.rows); screen.refresh(); term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; } } DisplayAction::ShowHelp => { let overlay_index = screens.overlay_index + 1; let screen = screens.current(); let mut screen = Screen::new( LoadedFile::new_static( overlay_index, "HELP", help_text(screen.keymap())?.into_bytes(), event_sender.clone(), ) .into(), config.clone(), )?; let size = term.get_screen_size().map_err(Error::Termwiz)?; screen.resize(size.cols, size.rows); screen.refresh(); term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; screens.overlay = Some(screen); screens.overlay_index = overlay_index; } DisplayAction::ClearOverlay => { screens.overlay = None; let screen = screens.current(); let size = term.get_screen_size().map_err(Error::Termwiz)?; screen.resize(size.cols, size.rows); screen.refresh(); term.render(&screen.render(&caps)).map_err(Error::Termwiz)?; } DisplayAction::Quit => { let screen = screens.current(); overlay_height.store(screen.overlay_height(), Ordering::SeqCst); return Ok(()); } } } } } sapling-streampager-0.11.0/src/error.rs000064400000000000000000000054751046102023000161510ustar 00000000000000//! Error types. use std::ffi::OsStr; use std::result::Result as StdResult; use std::sync::mpsc::RecvError; use std::sync::mpsc::RecvTimeoutError; use std::sync::mpsc::SendError; use std::sync::mpsc::TryRecvError; use thiserror::Error; /// Convenient return type for functions. pub type Result = StdResult; /// Main error type. #[derive(Debug, Error)] pub enum Error { /// Comes from [Termwiz](https://crates.io/crates/termwiz). #[error("terminal error")] Termwiz(#[source] termwiz::Error), /// Comes from [Regex](https://github.com/rust-lang/regex). #[error("regex error")] Regex(#[from] regex::Error), /// Generic I/O error. #[error("i/o error")] Io(#[from] std::io::Error), /// Returned when persisting a temporary file fails. #[error(transparent)] TempfilePersist(#[from] tempfile::PersistError), /// Keymap-related error. #[error("keymap error")] Keymap(#[from] crate::keymap_error::KeymapError), /// Binding-related error. #[error("keybinding error")] Binding(#[from] crate::bindings::BindingError), /// Generic formatting error. #[error(transparent)] Fmt(#[from] std::fmt::Error), /// Receive error on a channel. #[error("channel error")] ChannelRecv(#[from] RecvError), /// Try-receive error on a channel. #[error("channel error")] ChannelTryRecv(#[from] TryRecvError), /// Receive-timeout error on a channel. #[error("channel error")] ChannelRecvTimeout(#[from] RecvTimeoutError), /// Send error on a channel. #[error("channel error")] ChannelSend, /// Error returned if the terminfo database is missing. #[error("terminfo database not found (is $TERM correct?)")] TerminfoDatabaseMissing, /// Wrapped error within the context of a command. #[error("error running command '{command}'")] WithCommand { /// Wrapped error. #[source] error: Box, /// Command the error is about. command: String, }, /// Wrapped error within the context of a file. #[error("error loading file '{file}'")] WithFile { /// Wrapped error. #[source] error: Box, /// File the error is about. file: String, }, } impl Error { #[cfg(feature = "load_file")] pub(crate) fn with_file(self, file: impl AsRef) -> Self { Self::WithFile { error: Box::new(self), file: file.as_ref().to_owned(), } } pub(crate) fn with_command(self, command: impl AsRef) -> Self { Self::WithCommand { error: Box::new(self), command: command.as_ref().to_string_lossy().to_string(), } } } impl From> for Error { fn from(_send_error: SendError) -> Error { Error::ChannelSend } } sapling-streampager-0.11.0/src/event.rs000064400000000000000000000111541046102023000161300ustar 00000000000000//! Events. use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use std::sync::mpsc; use std::sync::Arc; use std::time::Duration; use termwiz::input::InputEvent; use termwiz::terminal::Terminal; use termwiz::terminal::TerminalWaker; use crate::action::Action; use crate::action::ActionSender; use crate::error::Error; use crate::file::FileIndex; /// An event. /// /// Events drive most of the main processing of `sp`. This includes user /// input, state changes, and display refresh requests. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum Event { /// An action. Action(Action), /// An input event. Input(InputEvent), /// A file has finished loading. Loaded(FileIndex), #[cfg(feature = "load_file")] /// A file has started loading more data. Appending(FileIndex), /// A file has started reloading. Reloading(FileIndex), /// Render an update to the screen. Render, /// Refresh the whole screen. Refresh, /// Refresh the overlay. RefreshOverlay, /// A new progress display is available. Progress, /// Search has found the first match. SearchFirstMatch(FileIndex), /// Search has finished. SearchFinished(FileIndex), } #[derive(Debug, Clone)] pub(crate) struct UniqueInstance(Arc); impl UniqueInstance { pub(crate) fn new() -> UniqueInstance { UniqueInstance(Arc::new(AtomicBool::new(false))) } } pub(crate) enum Envelope { Normal(Event), Unique(Event, UniqueInstance), } /// An event sender endpoint. #[derive(Clone)] pub(crate) struct EventSender(mpsc::Sender, TerminalWaker); impl EventSender { pub(crate) fn send(&self, event: Event) -> Result<(), Error> { self.0.send(Envelope::Normal(event))?; self.1.wake()?; Ok(()) } pub(crate) fn send_unique(&self, event: Event, unique: &UniqueInstance) -> Result<(), Error> { if unique .0 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) .is_ok() { self.0.send(Envelope::Unique(event, unique.clone()))?; self.1.wake()?; } Ok(()) } } /// An event stream. This is a wrapper multi-producer, single-consumer /// stream of `Event`s. pub(crate) struct EventStream { send: mpsc::Sender, recv: mpsc::Receiver, waker: TerminalWaker, } impl EventStream { /// Create a new event stream. pub(crate) fn new(waker: TerminalWaker) -> EventStream { let (send, recv) = mpsc::channel(); EventStream { send, recv, waker } } /// Create a sender for the event stream. pub(crate) fn sender(&self) -> EventSender { EventSender(self.send.clone(), self.waker.clone()) } /// Create an action sender for the event stream. pub(crate) fn action_sender(&self) -> ActionSender { ActionSender::new(self.sender()) } /// Attempt to receive an event. If timeout is specified, wait up to timeout /// for an event, returning None if there is no event. With no timeout, /// return None immediately if there is no event. pub(crate) fn try_recv(&self, timeout: Option) -> Result, Error> { let envelope = match timeout { Some(timeout) => match self.recv.recv_timeout(timeout) { Ok(envelope) => envelope, Err(mpsc::RecvTimeoutError::Timeout) => return Ok(None), Err(e) => return Err(e.into()), }, None => match self.recv.try_recv() { Ok(envelope) => envelope, Err(mpsc::TryRecvError::Empty) => return Ok(None), Err(e) => return Err(e.into()), }, }; match envelope { Envelope::Normal(event) => Ok(Some(event)), Envelope::Unique(event, unique) => { unique.0.store(false, Ordering::SeqCst); Ok(Some(event)) } } } /// Get an event, either from the event stream or from the terminal. pub(crate) fn get( &self, term: &mut dyn Terminal, wait: Option, ) -> Result, Error> { loop { if let Some(event) = self.try_recv(None)? { return Ok(Some(event)); } // The queue is empty. Try to get an input event from the terminal. match term.poll_input(wait).map_err(Error::Termwiz)? { Some(InputEvent::Wake) => {} Some(input_event) => return Ok(Some(Event::Input(input_event))), None => return Ok(None), } } } } sapling-streampager-0.11.0/src/file.rs000064400000000000000000000030711046102023000157250ustar 00000000000000//! Files. use std::borrow::Cow; use enum_dispatch::enum_dispatch; pub(crate) use crate::control::ControlledFile; pub(crate) use crate::loaded_file::LoadedFile; /// An identifier for a file streampager is paging. pub type FileIndex = usize; /// Default value for `needed_lines`. pub(crate) const DEFAULT_NEEDED_LINES: usize = 5000; /// Trait for getting information from a file. #[enum_dispatch] pub(crate) trait FileInfo { /// The file's index. fn index(&self) -> FileIndex; /// The file's title. fn title(&self) -> Cow<'_, str>; /// The file's info. fn info(&self) -> Cow<'_, str>; /// True once the file is loaded and all newlines have been parsed. fn loaded(&self) -> bool; /// Returns the number of lines in the file. fn lines(&self) -> usize; /// Runs the `call` function, passing it the contents of line `index`. /// Tries to avoid copying the data if possible, however the borrowed /// line only lasts as long as the function call. fn with_line(&self, index: usize, call: F) -> Option where F: FnMut(Cow<'_, [u8]>) -> T; /// Set how many lines are needed. /// /// If `self.lines()` exceeds that number, pause loading until /// `set_needed_lines` is called with a larger number. /// This is only effective for "streamed" input. fn set_needed_lines(&self, lines: usize); /// True if the loading thread has been paused. fn paused(&self) -> bool; } /// A file. #[enum_dispatch(FileInfo)] #[derive(Clone)] pub(crate) enum File { LoadedFile, ControlledFile, } sapling-streampager-0.11.0/src/help.rs000064400000000000000000000062551046102023000157450ustar 00000000000000//! Help screen use std::fmt::Write; use termwiz::input::{KeyCode, Modifiers}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use crate::bindings::{Category, Keymap}; use crate::error::Result; fn write_key_names(text: &mut String, keys: &[(Modifiers, KeyCode)]) -> Result { let mut w = 0; for (index, (modifiers, keycode)) in keys.iter().enumerate() { if index > 0 { if index == keys.len() - 1 { text.push_str("\x1B[0;2m or "); w += 4; } else { text.push_str("\x1B[0;2m, "); w += 2; } } text.push_str("\x1B[1m"); for (modifier, desc) in [ (Modifiers::CTRL, "Ctrl-"), (Modifiers::ALT, "Alt-"), (Modifiers::SUPER, "Super-"), (Modifiers::SHIFT, "Shift-"), ] .iter() { if modifiers.contains(*modifier) { text.push_str(desc); w += desc.width(); } } match keycode { KeyCode::Char(' ') => { text.push_str("Space"); w += 5; } KeyCode::Char(c) => { text.push(*c); w += c.width().unwrap_or(0); } KeyCode::Function(n) => { let n_string = n.to_string(); text.push('F'); text.push_str(&n_string); w += n_string.width() + 1; } KeyCode::UpArrow => { text.push_str("Up"); w += 2; } KeyCode::DownArrow => { text.push_str("Down"); w += 4; } KeyCode::LeftArrow => { text.push_str("Left"); w += 4; } KeyCode::RightArrow => { text.push_str("Right"); w += 5; } keycode => { let mut key_string = String::new(); write!(key_string, "{:?}", keycode)?; text.push_str(&key_string); w += key_string.width(); } } } text.push_str("\x1B[m"); Ok(w) } pub(crate) fn help_text(keymap: &Keymap) -> Result { let mut text = String::from( "\n \x1B[1;3;36;38;5;39mStream Pager\x1B[m \x1B[35;38;57m(\x1B[1msp\x1B[22m)\n", ); let prefix = " "; for category in Category::categories() { let mut title = false; for (binding, keys) in keymap.iter_keys() { if binding.category() == category { if !title { write!(text, "\n \x1B[1;4;33;38;5;130m{}\x1B[m\n\n", category)?; title = true; } text.push_str(" "); let w = write_key_names(&mut text, keys)?; if w < 34 { text.push_str(&prefix[w..]); } else { text.push_str("\n "); text.push_str(prefix); } writeln!(text, "{}", binding)?; } } } Ok(text) } sapling-streampager-0.11.0/src/keymap.pest000064400000000000000000000011271046102023000166230ustar 00000000000000WHITESPACE = _{ " " | "\t" | "\x0C" } COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* } ident = @{ ASCII_ALPHA ~ (ASCII_ALPHA | ASCII_DIGIT)* } char = { ANY } modifier = @{ "SUPER" | "CTRL" | "ALT" | "SHIFT" } keycode = ${ ident | "\'" ~ "\\"? ~ char ~ "\'" } visible_key = { modifier* ~ keycode } invisible_key = { "(" ~ modifier* ~ keycode ~ ")" } key = { visible_key | invisible_key } binding_param = @{ ASCII_DIGIT+ } binding = { ident ~ ( "(" ~ binding_param ~ ( "," ~ binding_param )* ~ ")" )? } item = { key ~ ("," ~ key)* ~ "=>" ~ binding ~ ";" } file = { SOI ~ (item? ~ NEWLINE)* ~ EOI } sapling-streampager-0.11.0/src/keymap_error.rs000064400000000000000000000026771046102023000175200ustar 00000000000000//! Errors specific to keymaps. use std::path::{Path, PathBuf}; use thiserror::Error; /// Errors specific to keymaps. #[derive(Debug, Error)] pub enum KeymapError { /// Error when encountering an unknown modifier. #[error("unknown modifier: {0}")] UnknownModifier(String), /// Error when a key definition is missing. #[error("key definition missing")] MissingDefinition, /// Error when a keymap is missing. #[error("keymap not found: {0}")] MissingKeymap(String), /// Error when a key is unrecognised. #[error("unrecognised key: {0}")] UnknownKey(String), /// Parsing error. #[cfg(feature = "keymap-file")] #[error("parse error: {0}")] Parse(#[from] pest::error::Error), /// Error related to parsing a binding within a keymap. #[error("keybinding error")] Binding(#[from] crate::bindings::BindingError), /// Wrapped error within the context of a file. #[error("error loading file '{file}'")] WithFile { /// Wrapped error. #[source] error: Box, /// File the error is about. file: PathBuf, }, } impl KeymapError { #[allow(unused)] pub(crate) fn with_file(self, file: impl AsRef) -> Self { Self::WithFile { error: Box::new(self), file: file.as_ref().to_owned(), } } } pub(crate) type Result = std::result::Result; sapling-streampager-0.11.0/src/keymap_macro.rs000064400000000000000000000143111046102023000174540ustar 00000000000000//! Keymap macro // Keymap macro implementation. // // Token-tree muncher: { rest } ( visible ) ( modifiers ) ( keys ) [ data ] // // Consumes definition from 'rest'. Modifiers are accumulated in 'modifiers'. Key definitions are // accumulated in 'keys'. Bindings are accumulated in 'data'. macro_rules! keymap_impl { // Base case: generate keymap data. ( {} ( $visible:literal ) () () $data:tt ) => { pub(crate) static KEYMAP: $crate::keymaps::KeymapData = &$data; }; // , (consume comma between keys) ( { , $( $rest:tt )* } ( $visible:literal ) ( ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( $visible ) ( ) ( $( $keys )* ) [ $( $data )* ] } }; // => Binding (termination) ( { => $action:ident $( ( $( $action_params:tt )* ) )? ; $( $rest:tt )* } ( $visible:literal ) ( ) ( ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( $visible ) ( ) ( ) [ $( $data )* ] } }; // => Binding (assign key) ( { => $action:ident $( ( $( $action_params:tt )* ) )? ; $( $rest:tt )* } ( $visible:literal ) ( ) ( $key:tt $key_visible:literal $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { => $action $( ( $( $action_params )* ) )? ; $( $rest )* } ( $visible ) ( ) ( $( $keys )* ) [ $( $data )* ( $key, $crate::bindings::BindingConfig { binding: $crate::bindings::Binding::Action( $crate::action::Action::$action $( ( $( $action_params )* ) )? ), visible: $key_visible, }, ), ] } }; // CTRL ( { CTRL $( $rest:tt )* } ( $visible:literal ) ( $( $modifier:ident )* ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( $visible ) ( $( $modifier )* CTRL ) ( $( $keys )* ) [ $( $data )* ] } }; // SHIFT ( { SHIFT $( $rest:tt )* } ( $visible:literal ) ( $( $modifier:ident )* ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( $visible ) ( $( $modifier )* SHIFT ) ( $( $keys )* ) [ $( $data )* ] } }; // ALT ( { ALT $( $rest:tt )* } ( $visible:literal ) ( $( $modifier:ident )* ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( $visible ) ( $( $modifier )* ALT ) ( $( $keys )* ) [ $( $data )* ] } }; // SUPER ( { SUPER $( $rest:tt )* } ( $visible:literal ) ( $( $modifier:ident )* ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( $visible ) ( $( $modifier )* SUPER ) ( $( $keys )* ) [ $( $data )* ] } }; // Character key (e.g. 'c') ( { $key:literal $( $rest:tt )* } ( $visible:literal ) ( $( $modifier:ident )* ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( true ) ( ) ( $( $keys )* ( termwiz::input::Modifiers::from_bits_truncate( $( termwiz::input::Modifiers::$modifier.bits() | )* termwiz::input::Modifiers::NONE.bits() ), termwiz::input::KeyCode::Char($key), ) $visible ) [ $( $data )* ] } }; // F ( { F $num:literal $( $rest:tt )* } ( $visible:literal ) ( $( $modifier:ident )* ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( true ) ( ) ( $( $keys )* ( termwiz::input::Modifiers::from_bits_truncate( $( termwiz::input::Modifiers::$modifier.bits() | )* termwiz::input::Modifiers::NONE.bits() ), termwiz::input::KeyCode::Function($num), ) $visible ) [ $( $data )* ] } }; // KeyCode ( { $key:ident $( $rest:tt )* } ( $visible:literal ) ( $( $modifier:ident )* ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $rest )* } ( true ) ( ) ( $( $keys )* ( termwiz::input::Modifiers::from_bits_truncate( $( termwiz::input::Modifiers::$modifier.bits() | )* termwiz::input::Modifiers::NONE.bits() ), termwiz::input::KeyCode::$key, ) $visible ) [ $( $data )* ] } }; // ( hidden binding ) ( { ( $( $bind:tt )* ) $( $rest:tt )* } ( $visible:literal ) ( $( $modifier:ident )* ) ( $( $keys:tt )* ) [ $( $data:tt )* ] ) => { keymap_impl! { { $( $bind )* $( $rest )* } ( false ) ( $( $modifier )* ) ( $( $keys )* ) [ $( $data )* ] } }; } macro_rules! keymap { ( $( $all:tt )* ) => { keymap_impl! { { $( $all )* } (true) () () [] } }; } sapling-streampager-0.11.0/src/keymaps/default.rs000064400000000000000000000033431046102023000201050ustar 00000000000000//! Default keymap keymap! { CTRL 'c', 'q', ('Q') => Quit; Escape => Cancel; CTRL 'l', 'r' => Refresh; CTRL 'r' => ToggleRuler; UpArrow, 'k', (CTRL 'k'), (CTRL 'p') => ScrollUpLines(1); DownArrow, 'j', (CTRL 'n'), Enter => ScrollDownLines(1); SHIFT UpArrow, (ApplicationUpArrow) => ScrollUpScreenFraction(4); SHIFT DownArrow, (ApplicationDownArrow) => ScrollDownScreenFraction(4); CTRL UpArrow, 'u', CTRL 'u' => ScrollUpScreenFraction(2); CTRL DownArrow, 'd', CTRL 'd' => ScrollDownScreenFraction(2); PageUp, Backspace, 'b', CTRL 'b', ALT 'v' => ScrollUpScreenFraction(1); PageDown, ' ', 'f', CTRL 'f', CTRL 'v' => ScrollDownScreenFraction(1); Home, 'g', '<' => ScrollToTop; End, 'F', 'G', '>' => ScrollToBottom; LeftArrow => ScrollLeftColumns(4); RightArrow => ScrollRightColumns(4); SHIFT LeftArrow => ScrollLeftScreenFraction(4); SHIFT RightArrow => ScrollRightScreenFraction(4); '[', SHIFT Tab => PreviousFile; ']', Tab => NextFile; 'h', F 1 => Help; '#' => ToggleLineNumbers; '\\' => ToggleLineWrapping; ':', '%' => PromptGoToLine; '/' => PromptSearchForwards; '?' => PromptSearchBackwards; ',' => PreviousMatch; '.' => NextMatch; 'p', ('N') => PreviousMatchScreen; 'n' => NextMatchScreen; '(' => FirstMatch; ')' => LastMatch; '0' => AppendDigitToRepeatCount(0); '1' => AppendDigitToRepeatCount(1); '2' => AppendDigitToRepeatCount(2); '3' => AppendDigitToRepeatCount(3); '4' => AppendDigitToRepeatCount(4); '5' => AppendDigitToRepeatCount(5); '6' => AppendDigitToRepeatCount(6); '7' => AppendDigitToRepeatCount(7); '8' => AppendDigitToRepeatCount(8); '9' => AppendDigitToRepeatCount(9); } sapling-streampager-0.11.0/src/keymaps.rs000064400000000000000000000025311046102023000164570ustar 00000000000000//! Keymaps use termwiz::input::{KeyCode, Modifiers}; use crate::bindings::{BindingConfig, Keymap}; use crate::keymap_error::{KeymapError, Result}; // Static data to generate a keymap. type KeymapData = &'static [((Modifiers, KeyCode), BindingConfig)]; macro_rules! keymaps { ( $( $visibility:vis mod $name:ident ; )* ) => { $( $visibility mod $name ; )* pub(crate) static KEYMAPS: &'static [(&'static str, $crate::keymaps::KeymapData)] = &[ $( ( stringify!( $name ), $crate::keymaps::$name::KEYMAP ), )* ]; } } keymaps! { pub(crate) mod default; } pub(crate) fn load(name: &str) -> Result { for (keymap_name, keymap_data) in KEYMAPS { if &name == keymap_name { return Ok(Keymap::from(keymap_data.iter())); } } #[cfg(feature = "keymap-file")] { if let Some(mut path) = dirs::config_dir() { path.push("streampager"); path.push("keymaps"); path.push(name); if let Ok(keymap_data) = std::fs::read_to_string(&path) { let keymap_file = crate::keymap_file::KeymapFile::parse(&keymap_data) .map_err(|err| err.with_file(path))?; return Ok(Keymap::from(keymap_file.iter())); } } } Err(KeymapError::MissingKeymap(name.to_string())) } sapling-streampager-0.11.0/src/lib.rs000064400000000000000000000013521046102023000155540ustar 00000000000000//! Stream Pager //! //! A pager for streams. #![warn(missing_docs)] #![recursion_limit = "1024"] #![allow(clippy::comparison_chain)] pub mod action; mod bar; pub mod bindings; mod buffer; #[cfg(feature = "load_file")] mod buffer_cache; mod command; pub mod config; pub mod control; mod direct; mod display; pub mod error; mod event; pub mod file; mod help; mod keymap_error; #[cfg(feature = "keymap-file")] mod keymap_file; #[macro_use] mod keymap_macro; mod keymaps; mod line; mod line_cache; mod line_drawing; mod loaded_file; mod overstrike; pub mod pager; mod progress; mod prompt; mod prompt_history; mod refresh; mod ruler; mod screen; mod search; mod util; pub use error::{Error, Result}; pub use file::FileIndex; pub use pager::Pager; sapling-streampager-0.11.0/src/line.rs000064400000000000000000001100501046102023000157310ustar 00000000000000//! Lines in a file. use std::borrow::Cow; use std::cmp::Ordering; use std::str; use std::sync::{Arc, Mutex}; use std::num::NonZeroUsize; use lru::LruCache; use regex::bytes::{NoExpand, Regex}; use smallvec::SmallVec; use termwiz::cell::{CellAttributes, Intensity}; use termwiz::color::{AnsiColor, ColorAttribute}; use termwiz::escape::csi::{Edit, EraseInLine, Sgr, CSI}; use termwiz::escape::esc::{Esc, EscCode}; use termwiz::escape::osc::OperatingSystemCommand; use termwiz::escape::parser::Parser; use termwiz::escape::Action; use termwiz::hyperlink::Hyperlink; use termwiz::surface::{change::Change, Position}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use crate::config::WrappingMode; use crate::line_drawing; use crate::overstrike; use crate::search::{trim_trailing_newline, ESCAPE_SEQUENCE}; use crate::util; const LEFT_ARROW: &str = "<"; const RIGHT_ARROW: &str = ">"; const TAB_SPACES: &str = " "; const WRAPS_CACHE_SIZE: usize = 4; /// Line wrap in the cache are uniquely identified by index and wrapping mode. type WrapCacheIndex = (usize, WrappingMode); /// Line wraps in the cache are represented by a list of start and end offsets. type WrapCacheItem = Vec<(usize, usize)>; /// Line wraps in the cache are represented by a list of start and end offsets. type WrapCacheItemRef<'a> = &'a [(usize, usize)]; /// Represents a single line in a displayed file. #[derive(Debug, Clone)] pub(crate) struct Line { spans: Box<[Span]>, wraps: Arc>>, } /// Style that is being applied. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum OutputStyle { /// The source file's output style. File, /// Control characters style (inverse video). Control, /// A search match. Match, /// The currently selected search match. CurrentMatch, } /// Tracker of current attributes state. struct AttributeState { /// Current attributes for the file attrs: CellAttributes, /// Whether DEC line drawing mode is currently enabled line_drawing: bool, /// Whether the file's attributes have changed changed: bool, /// What the currently applied style is. style: OutputStyle, /// What color the end of the line should be end_of_line: ColorAttribute, } impl AttributeState { /// Create a new color state tracker. fn new() -> AttributeState { AttributeState { attrs: CellAttributes::default(), line_drawing: false, changed: false, style: OutputStyle::File, end_of_line: ColorAttribute::default(), } } /// Apply a sequence of Sgr escape codes onto the attribute state. fn apply_sgr_sequence(&mut self, sgr_sequence: &[Sgr]) { for sgr in sgr_sequence.iter() { match *sgr { Sgr::Reset => { // Reset doesn't clear the hyperlink. let hyperlink = self.attrs.hyperlink().cloned(); self.attrs = CellAttributes::default(); self.attrs.set_hyperlink(hyperlink); } Sgr::Intensity(intensity) => { self.attrs.set_intensity(intensity); } Sgr::Underline(underline) => { self.attrs.set_underline(underline); } Sgr::Blink(blink) => { self.attrs.set_blink(blink); } Sgr::Italic(italic) => { self.attrs.set_italic(italic); } Sgr::Inverse(inverse) => { self.attrs.set_reverse(inverse); } Sgr::Invisible(invis) => { self.attrs.set_invisible(invis); } Sgr::StrikeThrough(strike) => { self.attrs.set_strikethrough(strike); } Sgr::Foreground(color) => { self.attrs.set_foreground(color); } Sgr::Background(color) => { self.attrs.set_background(color); } Sgr::Font(_) => {} Sgr::UnderlineColor(color) => { self.attrs.set_underline_color(color); } Sgr::Overline(enable) => { self.attrs.set_overline(enable); } Sgr::VerticalAlign(align) => { self.attrs.set_vertical_align(align); } } } self.changed = true; } /// Apply a hyperlink escape code onto the attribute state. fn apply_hyperlink(&mut self, hyperlink: &Option>) { self.attrs.set_hyperlink(hyperlink.clone()); self.changed = true; } /// Switch to the given style. The correct escape color sequences will be emitted. fn style(&mut self, style: OutputStyle) -> Option { if self.style != style || self.changed { let attrs = match style { OutputStyle::File => self.attrs.clone(), OutputStyle::Control => CellAttributes::default().set_reverse(true).clone(), OutputStyle::Match => self .attrs .clone() .set_foreground(AnsiColor::Black) .set_background(AnsiColor::Olive) .set_intensity(Intensity::Normal) .clone(), OutputStyle::CurrentMatch => self .attrs .clone() .set_foreground(AnsiColor::Black) .set_background(AnsiColor::Teal) .set_intensity(Intensity::Normal) .clone(), }; self.style = style; self.changed = false; Some(Change::AllAttributes(attrs)) } else { None } } } /// A span of text within a line. #[derive(Debug, Clone, PartialEq, Eq)] enum Span { /// Ordinary text. Text(String), /// Text that matches the current search, and the search match index. Match(String, usize), /// A control character. Control(u8), /// An invalid UTF-8 byte. Invalid(u8), /// An unprintable unicode grapheme cluster. Unprintable(String), /// A sequence of SGR escape codes. SgrSequence(SmallVec<[Sgr; 5]>), /// A hyperlink escape code. Hyperlink(Option>), /// A DEC line drawing mode escape code. LineDrawing(bool), /// Data that should be ignored. Ignore(SmallVec<[u8; 20]>), /// A tab control character. Tab, /// A terminating CRLF sequence. CrLf, /// A terminating LF sequence. Lf, /// An erase-to-end-of-line sequence. EraseToEndOfLine, } /// Produce `Change`s to output some text in the given style at the given /// position, truncated to the start and end columns. /// /// Returns the new position after the text has been rendered. fn write_truncated( changes: &mut Vec, attr_state: &mut AttributeState, style: OutputStyle, text: &str, start: usize, end: usize, position: usize, ) -> usize { let text_width = text.width(); if position + text_width > start && position < end { if let Some(change) = attr_state.style(style) { changes.push(change); } let start = start.saturating_sub(position); let end = end.saturating_sub(position); changes.push(Change::Text(util::truncate_string( text, start, end - start, ))); } position + text_width } struct SplitWords<'t> { text: &'t str, } impl<'t> SplitWords<'t> { fn new(text: &'t str) -> Self { SplitWords { text } } } impl<'t> Iterator for SplitWords<'t> { type Item = (&'t str, &'t str); fn next(&mut self) -> Option { let text = self.text; if text.is_empty() { return None; } for (i, ch) in text.char_indices() { if ch.is_whitespace() { for (j, ch) in text[i..].char_indices() { if !ch.is_whitespace() { self.text = &text[i + j..]; return Some((&text[..i], &text[i..i + j])); } } let end = text.len(); self.text = &text[end..end]; return Some((&text[..i], &text[i..])); } if ch == '-' { let j = i + 1; self.text = &text[j..]; return Some((&text[..j], &text[j..j])); } } let end = text.len(); self.text = &text[end..end]; Some((text, &text[end..end])) } } impl Span { /// Render the span at the given position in the terminal. fn render( &self, changes: &mut Vec, attr_state: &mut AttributeState, start: usize, end: usize, mut position: usize, search_index: Option, ) -> usize { match *self { Span::Text(ref t) => { let text = if attr_state.line_drawing { Cow::Owned(line_drawing::convert_line_drawing(t.as_str())) } else { Cow::Borrowed(t.as_str()) }; position = write_truncated( changes, attr_state, OutputStyle::File, text.as_ref(), start, end, position, ); } Span::Match(ref t, ref match_index) => { let style = if search_index == Some(*match_index) { OutputStyle::CurrentMatch } else { OutputStyle::Match }; let text = if attr_state.line_drawing { Cow::Owned(line_drawing::convert_line_drawing(t.as_str())) } else { Cow::Borrowed(t.as_str()) }; position = write_truncated( changes, attr_state, style, text.as_ref(), start, end, position, ); } Span::Tab => { let tabchars = 8 - position % 8; position = write_truncated( changes, attr_state, OutputStyle::File, &TAB_SPACES[..tabchars], start, end, position, ); } Span::Control(c) | Span::Invalid(c) => { position = write_truncated( changes, attr_state, OutputStyle::Control, &format!("<{:02X}>", c), start, end, position, ); } Span::Unprintable(ref grapheme) => { for c in grapheme.chars() { position = write_truncated( changes, attr_state, OutputStyle::Control, &format!("", c as u32), start, end, position, ); } } Span::SgrSequence(ref s) => attr_state.apply_sgr_sequence(s), Span::Hyperlink(ref l) => attr_state.apply_hyperlink(l), Span::LineDrawing(e) => attr_state.line_drawing = e, Span::EraseToEndOfLine => attr_state.end_of_line = attr_state.attrs.background(), _ => {} } position } fn split( &self, rows: &mut Vec<(usize, usize)>, start: usize, position: usize, width: usize, words: bool, ) -> (usize, usize) { match self { Span::Text(text) | Span::Match(text, _) => { let mut start = start; let mut position = position; if words { for (word, sep) in SplitWords::new(text) { let end = position + word.width() + sep.width(); if end - start <= width { // This word fits within this row position = end; } else { // This word wraps to the next row. if start != position { // Add the existing words to the row. rows.push((start, position)); start = position; } if end - start > width { // This word is at the start of the row and is longer than the whole // row. Break it at grapheme boundaries. for grapheme in word.graphemes(true).chain(sep.graphemes(true)) { let end = position + grapheme.width(); if end - start <= width { // This character fits within this row position = end; } else { // This character wraps to the next row rows.push((start, position)); start = position; position = end; } } } else { position = end; } } } } else { for grapheme in text.graphemes(true) { let end = position + grapheme.width(); if end - start <= width { // This character fits within this row position = end; } else { // This character wraps to the next row rows.push((start, position)); start = position; position = end; } } } (start, position) } Span::Tab => { let tabchars = 8 - position % 8; let end = position + tabchars; if end - start <= width { // This tab fits within this row (start, end) } else { // This tab completes the row rows.push((start, end)); (end, end) } } Span::Control(_) | Span::Invalid(_) => { let end = position + 4; if end - start <= width { // This character fits within this row (start, end) } else { // This character wraps to the next row rows.push((start, position)); (position, end) } } Span::Unprintable(_) => { let end = position + 8; if end - start <= width { // This character fits within this row (start, end) } else { // This character wraps to the next row rows.push((start, position)); (position, end) } } _ => (start, position), } } } /// Parse data into an array of Spans. fn parse_spans(data: &[u8], match_index: Option) -> Vec { let mut spans = Vec::new(); let mut input = data; fn parse_unicode_span(data: &str, spans: &mut Vec, match_index: Option) { let mut text_start = None; let mut skip_to = None; for (index, grapheme) in data.grapheme_indices(true) { let mut span = None; // Skip past any escape sequence we've already extracted if let Some(end) = skip_to { if index < end { continue; } else { skip_to = None; } } if grapheme == "\x1B" { // Look ahead for an escape sequence let mut parser = Parser::new(); let bytes = data.as_bytes(); if let Some((actions, len)) = parser.parse_first_as_vec(&bytes[index..]) { // Look at the sequence of actions this parsed to. We // assume this is one of: // - A sequence of SGR actions parse from a single SGR // sequence. // - A single Cursor or Edit action we want to ignore. // - A single OSC that contains a hyperlink. // - Something else that we don't want to parse. let mut actions = actions.into_iter(); match actions.next() { Some(Action::CSI(CSI::Sgr(sgr))) => { // Collect all Sgr values let mut sgr_sequence = SmallVec::new(); sgr_sequence.push(sgr); for action in actions { if let Action::CSI(CSI::Sgr(sgr)) = action { sgr_sequence.push(sgr); } } span = Some(Span::SgrSequence(sgr_sequence)); skip_to = Some(index + len); } Some(Action::CSI(CSI::Edit(Edit::EraseInLine( EraseInLine::EraseToEndOfLine, )))) => { span = Some(Span::EraseToEndOfLine); skip_to = Some(index + len); } Some(Action::CSI(CSI::Cursor(_))) | Some(Action::CSI(CSI::Edit(_))) => { span = Some(Span::Ignore(SmallVec::from_slice( &bytes[index..index + len], ))); skip_to = Some(index + len); } Some(Action::OperatingSystemCommand(osc)) => { if let OperatingSystemCommand::SetHyperlink(hyperlink) = *osc { span = Some(Span::Hyperlink(hyperlink.map(Arc::new))); skip_to = Some(index + len); } } Some(Action::Esc(Esc::Code(code))) => match code { EscCode::DecLineDrawingG0 | EscCode::AsciiCharacterSetG0 => { span = Some(Span::LineDrawing(code == EscCode::DecLineDrawingG0)); skip_to = Some(index + len); } _ => {} }, _ => {} } } } if grapheme == "\r\n" { span = Some(Span::CrLf); skip_to = Some(index + 2); } if grapheme == "\n" { span = Some(Span::Lf); } if grapheme == "\t" { span = Some(Span::Tab); } if span.is_none() && grapheme.len() == 1 { if let Some(ch) = grapheme.bytes().next() { if ch < b' ' || ch == b'\x7F' { span = Some(Span::Control(ch)); } } } if span.is_none() && grapheme.width() == 0 { span = Some(Span::Unprintable(grapheme.to_string())); } if let Some(span) = span { if let Some(start) = text_start { if let Some(match_index) = match_index { spans.push(Span::Match(data[start..index].to_string(), match_index)); } else { spans.push(Span::Text(data[start..index].to_string())); } text_start = None; } spans.push(span); } else if text_start.is_none() { text_start = Some(index); } } if let Some(start) = text_start { if let Some(match_index) = match_index { spans.push(Span::Match(data[start..].to_string(), match_index)); } else { spans.push(Span::Text(data[start..].to_string())); } } } loop { match str::from_utf8(input) { Ok(valid) => { parse_unicode_span(valid, &mut spans, match_index); break; } Err(error) => { let (valid, after_valid) = input.split_at(error.valid_up_to()); if !valid.is_empty() { unsafe { parse_unicode_span( str::from_utf8_unchecked(valid), &mut spans, match_index, ); } } if let Some(len) = error.error_len() { for byte in &after_valid[..len] { spans.push(Span::Invalid(*byte)); } input = &after_valid[len..]; } else { for byte in after_valid { spans.push(Span::Invalid(*byte)); } break; } } } } spans } impl Line { pub(crate) fn new(_index: usize, data: impl AsRef<[u8]>) -> Line { let data = overstrike::convert_overstrike(data.as_ref()); let spans = parse_spans(&data[..], None).into_boxed_slice(); let wraps = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(WRAPS_CACHE_SIZE).unwrap()))); Line { spans, wraps } } pub(crate) fn new_search(_index: usize, data: impl AsRef<[u8]>, regex: &Regex) -> Line { let data = overstrike::convert_overstrike(data.as_ref()); let len = trim_trailing_newline(data.as_ref()); let mut spans = Vec::new(); let mut start = 0; let (data_without_escapes, convert_offset) = if ESCAPE_SEQUENCE.is_match(&data[..len]) { let mut escape_ranges = Vec::new(); for match_range in ESCAPE_SEQUENCE.find_iter(&data[..len]) { escape_ranges.push((match_range.start(), match_range.end())); } ( ESCAPE_SEQUENCE.replace_all(&data[..len], NoExpand(b"")), Some(move |offset| { let mut original_offset = 0; let mut remaining_offset = offset; for (escape_start, escape_end) in escape_ranges.iter() { if original_offset + remaining_offset < *escape_start { break; } else { remaining_offset -= escape_start - original_offset; original_offset = *escape_end; } } original_offset + remaining_offset }), ) } else { (Cow::Borrowed(&data[..len]), None) }; for (match_index, match_range) in regex.find_iter(&data_without_escapes[..]).enumerate() { let (match_start, match_end) = if let Some(ref convert) = convert_offset { (convert(match_range.start()), convert(match_range.end())) } else { (match_range.start(), match_range.end()) }; if start < match_start { spans.append(&mut parse_spans(&data[start..match_start], None)); } spans.append(&mut parse_spans( &data[match_start..match_end], Some(match_index), )); start = match_end; } if start < data.len() { spans.append(&mut parse_spans(&data[start..], None)); } let spans = spans.into_boxed_slice(); let wraps = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(WRAPS_CACHE_SIZE).unwrap()))); Line { spans, wraps } } /// Produce the `Change`s needed to render a slice of the line on a terminal. pub(crate) fn render( &self, changes: &mut Vec, start: usize, end: usize, search_index: Option, ) { let mut start = start; let mut attr_state = AttributeState::new(); let mut position = 0; if start > 0 { changes.push(Change::AllAttributes( CellAttributes::default() .set_foreground(AnsiColor::Navy) .set_intensity(Intensity::Bold) .clone(), )); changes.push(LEFT_ARROW.into()); changes.push(Change::AllAttributes(CellAttributes::default())); start += 1; } for span in self.spans.iter() { position = span.render(changes, &mut attr_state, start, end, position, search_index); } match position.cmp(&end) { Ordering::Greater => { // There is more text after the end of the line, so we need to // render the right arrow. // // The cursor should be in the final column of the line. However, // we need to work around strange terminal behaviour when setting // styles at the end of the line by backspacing and then moving // forwards. changes.push(Change::Text("\x08".into())); changes.push(Change::CursorPosition { x: Position::Relative(1), y: Position::Relative(0), }); changes.push(Change::AllAttributes( CellAttributes::default() .set_foreground(AnsiColor::Navy) .set_intensity(Intensity::Bold) .clone(), )); changes.push(RIGHT_ARROW.into()); } Ordering::Less => changes.push(Change::ClearToEndOfLine(attr_state.end_of_line)), Ordering::Equal => {} } changes.push(Change::AllAttributes(CellAttributes::default())); } /// Produce the `Change`s needed to render a row of the wrapped line on a terminal. pub(crate) fn render_wrapped( &self, changes: &mut Vec, first_row: usize, row_count: usize, width: usize, wrapping: WrappingMode, search_index: Option, ) { let (start, end) = { fn wrap_bounds_for_rows( rows: WrapCacheItemRef<'_>, first_row: usize, row_count: usize, ) -> (usize, usize) { let end = rows .get(first_row + row_count - 1) .map_or_else(|| rows.last().map_or(0, |r| r.1), |r| r.1); let start = rows.get(first_row).map_or(end, |r| r.0); (start, end) } let mut wraps = self.wraps.lock().unwrap(); if let Some(rows) = wraps.get(&(width, wrapping)) { wrap_bounds_for_rows(rows, first_row, row_count) } else { let rows = self.make_wrap(width, wrapping); let (start, end) = wrap_bounds_for_rows(&rows, first_row, row_count); wraps.put((width, wrapping), rows); (start, end) } }; let mut attr_state = AttributeState::new(); let mut position = 0; for span in self.spans.iter() { position = span.render(changes, &mut attr_state, start, end, position, search_index); } if end - start < width * row_count { changes.push(Change::ClearToEndOfLine(attr_state.end_of_line)); } changes.push(Change::AllAttributes(CellAttributes::default())); } /// Returns the start and end pairs for each row of the line if wrapped. fn make_wrap(&self, width: usize, wrapping: WrappingMode) -> Vec<(usize, usize)> { let mut rows = Vec::new(); match wrapping { WrappingMode::Unwrapped => { rows.push((0, std::usize::MAX)); } WrappingMode::GraphemeBoundary | WrappingMode::WordBoundary => { let mut start = 0; let mut position = 0; for span in self.spans.iter() { let (new_start, new_position) = span.split( &mut rows, start, position, width, wrapping == WrappingMode::WordBoundary, ); start = new_start; position = new_position; } if position > start || rows.is_empty() { rows.push((start, position)) } } } rows } /// Returns the number of rows for this line if wrapped at the given width pub(crate) fn height(&self, width: usize, wrapping: WrappingMode) -> usize { if wrapping == WrappingMode::Unwrapped { return 1; } let mut wraps = self.wraps.lock().unwrap(); if let Some(rows) = wraps.get_mut(&(width, wrapping)) { return rows.len(); } let rows = self.make_wrap(width, wrapping); let height = rows.len(); wraps.put((width, wrapping), rows); height } } #[cfg(test)] mod test { use super::Span::*; use super::*; use termwiz::color::ColorSpec; #[test] fn test_parse_spans() { assert_eq!(parse_spans(b"hello", None), vec![Text("hello".to_string())]); assert_eq!( parse_spans("Wíth Únícódé".as_bytes(), None), vec![Text("Wíth Únícódé".to_string())] ); assert_eq!( parse_spans(b"Truncated\xE0", None), vec![Text("Truncated".to_string()), Invalid(224)] ); assert_eq!( parse_spans(b"Truncated\xE0\x80", None), vec![Text("Truncated".to_string()), Invalid(224), Invalid(128)] ); assert_eq!( parse_spans(b"Internal\xE0Error", None), vec![ Text("Internal".to_string()), Invalid(224), Text("Error".to_string()) ] ); assert_eq!( parse_spans(b"\x84StartingError", None), vec![Invalid(132), Text("StartingError".to_string())] ); assert_eq!( parse_spans(b"Internal\xE0\x80Error", None), vec![ Text("Internal".to_string()), Invalid(224), Invalid(128), Text("Error".to_string()) ] ); assert_eq!( parse_spans(b"TerminatingControl\x1F", None), vec![Text("TerminatingControl".to_string()), Control(31)] ); assert_eq!( parse_spans(b"Internal\x02Control", None), vec![ Text("Internal".to_string()), Control(2), Text("Control".to_string()) ] ); assert_eq!( parse_spans(b"\x1AStartingControl", None), vec![Control(26), Text("StartingControl".to_string())] ); assert_eq!( parse_spans(b"\x1B[1mBold!\x1B[m", None), vec![ SgrSequence(SmallVec::from(&[Sgr::Intensity(Intensity::Bold)][..])), Text("Bold!".to_string()), SgrSequence(SmallVec::from(&[Sgr::Reset][..])) ] ); assert_eq!( parse_spans( b"Multi\x1B[31;7m-colored \x1B[36;1mtext\x1B[42;1m line", None ), vec![ Text("Multi".to_string()), SgrSequence(SmallVec::from( &[ Sgr::Foreground(ColorSpec::PaletteIndex(1)), Sgr::Inverse(true) ][..] )), Text("-colored ".to_string()), SgrSequence(SmallVec::from( &[ Sgr::Foreground(ColorSpec::PaletteIndex(6)), Sgr::Intensity(Intensity::Bold) ][..] )), Text("text".to_string()), SgrSequence(SmallVec::from( &[ Sgr::Background(ColorSpec::PaletteIndex(2)), Sgr::Intensity(Intensity::Bold) ][..] )), Text(" line".to_string()) ] ); assert_eq!( parse_spans(b"Terminating LF\n", None), vec![Text("Terminating LF".to_string()), Lf] ); assert_eq!( parse_spans(b"Terminating CRLF\r\n", None), vec![Text("Terminating CRLF".to_string()), CrLf] ); assert_eq!( parse_spans(b"Terminating CR\r", None), vec![Text("Terminating CR".to_string()), Control(13)] ); assert_eq!( parse_spans(b"Internal\rCR", None), vec![ Text("Internal".to_string()), Control(13), Text("CR".to_string()) ] ); assert_eq!( parse_spans(b"Internal\nLF", None), vec![Text("Internal".to_string()), Lf, Text("LF".to_string())] ); assert_eq!( parse_spans(b"Internal\r\nCRLF", None), vec![Text("Internal".to_string()), CrLf, Text("CRLF".to_string())] ); } #[test] fn test_wrap() { let data = concat!( "A simple line with several words, including some superobnoxiously ", "big ones and some extra-confusingly-awkward hyphenated ones." ); let data_wrapped_10 = vec![ "A simple ", "line with ", "several ", "words, ", "including ", "some ", "superobnox", "iously ", "big ones ", "and some ", "extra-", "confusingl", "y-awkward ", "hyphenated", " ones.", ]; let line = Line::new(0, data.as_bytes()); assert_eq!( line.make_wrap(100, WrappingMode::Unwrapped), vec![(0, std::usize::MAX)], ); assert_eq!( line.make_wrap(40, WrappingMode::GraphemeBoundary), vec![(0, 40), (40, 80), (80, 120), (120, 126)], ); // The start and end values are positions, not string indices, but since data is pure ASCII // they will match. let line_wrapped_10: Vec<_> = line .make_wrap(10, WrappingMode::WordBoundary) .iter() .map(|(start, end)| &data[*start..*end]) .collect(); assert_eq!(line_wrapped_10, data_wrapped_10); // In this example, the control character doesn't fit into the 40 character width. let line = Line::new( 0, "Some line with Únícódé and \x1B[31mcolors\x1B[m and \x01Control characters\r\n" .as_bytes(), ); assert_eq!( line.make_wrap(40, WrappingMode::GraphemeBoundary), vec![(0, 38), (38, 60)], ); } } sapling-streampager-0.11.0/src/line_cache.rs000064400000000000000000000034331046102023000170620ustar 00000000000000//! Line Cache //! //! An LRU-cache for lines. use std::borrow::Cow; use std::num::NonZeroUsize; use lru::LruCache; use regex::bytes::Regex; use crate::file::{File, FileInfo}; use crate::line::Line; /// An LRU-cache for Lines. pub(crate) struct LineCache(LruCache); impl LineCache { /// Create a new LineCache with the given capacity. pub(crate) fn new(capacity: NonZeroUsize) -> LineCache { LineCache(LruCache::new(capacity)) } /// Get a line out of the line cache, or create it if it is not /// in the cache. pub(crate) fn get_or_create<'a>( &'a mut self, file: &File, line_index: usize, regex: Option<&Regex>, ) -> Option> { let cache = &mut self.0; if cache.contains(&line_index) { Some(Cow::Borrowed(cache.get_mut(&line_index).unwrap())) } else { let line = file.with_line(line_index, |line| { if let Some(regex) = regex { Line::new_search(line_index, line, regex) } else { Line::new(line_index, line) } }); if let Some(line) = line { // Don't cache the line if it's the last line of the file // and the file is still loading. It might not be complete. if file.loaded() || line_index + 1 < file.lines() { cache.put(line_index, line); Some(Cow::Borrowed(cache.get_mut(&line_index).unwrap())) } else { Some(Cow::Owned(line)) } } else { None } } } /// Clear all entries in the line cache. pub(crate) fn clear(&mut self) { self.0.clear(); } } sapling-streampager-0.11.0/src/line_drawing.rs000064400000000000000000000027061046102023000174540ustar 00000000000000//! DEC Line Drawing Mode Handling //! //! VT100 and VT220 terminals supported an alternate character set with //! additional characters, including line drawing characters. Switching //! to and from line drawing mode was signalled by escape sequences. //! //! Handle this by converting characters between escape sequence blocks //! into the equivalent unicode character. // Start replacing bytes after 0x5F. const REPLACEMENTS_START: usize = 0x5F; // The bytes starting with 0x5F are replaced with the following unicode strings. const UNICODE_REPLACEMENTS: &[&str] = &[ "\u{A0}", "◆", "▒", "␉", "␌", "␍", "␊", "°", "±", "␤", "␋", "┘", "┐", "┌", "└", "┼", "⎺", "⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", "│", "≤", "≥", "π", "≠", "£", "·", ]; pub(crate) fn convert_line_drawing(input: &str) -> String { let mut out = String::with_capacity(input.len()); let range = REPLACEMENTS_START..REPLACEMENTS_START + UNICODE_REPLACEMENTS.len(); for c in input.chars() { if range.contains(&(c as usize)) { out.push_str(UNICODE_REPLACEMENTS[(c as usize) - REPLACEMENTS_START]); } else { out.push(c); } } out } #[cfg(test)] mod test { use super::*; #[test] fn test_convert_line_drawing() { assert_eq!(convert_line_drawing("aaaaa"), "▒▒▒▒▒"); assert_eq!(convert_line_drawing("tquOKtqu"), "├─┤OK├─┤"); } } sapling-streampager-0.11.0/src/loaded_file.rs000064400000000000000000000774561046102023000172570ustar 00000000000000//! Loaded files. //! //! Files where the data is loaded from somewhere. use std::borrow::Cow; use std::cmp::max; #[cfg(feature = "load_file")] use std::cmp::min; use std::ffi::OsStr; #[cfg(feature = "load_file")] use std::fs::File as StdFile; use std::io::Read; #[cfg(feature = "load_file")] use std::io::{Seek, SeekFrom}; #[cfg(feature = "load_file")] use std::path::Path; use std::process::{Command, Stdio}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; #[cfg(feature = "load_file")] use std::sync::mpsc; use std::sync::{Arc, Condvar, Mutex, RwLock}; use std::thread; #[cfg(feature = "load_file")] use std::time::Duration; #[cfg(feature = "load_file")] use memmap2::Mmap; #[cfg(feature = "load_file")] use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; use crate::buffer::Buffer; #[cfg(feature = "load_file")] use crate::buffer_cache::BufferCache; use crate::error::{Error, Result}; use crate::event::{Event, EventSender}; #[cfg(feature = "load_file")] use crate::event::UniqueInstance; use crate::file::{FileIndex, FileInfo, DEFAULT_NEEDED_LINES}; /// Buffer size to use when loading and parsing files. This is also the block /// size when parsing memory mapped files or caching files read from disk. const BUFFER_SIZE: usize = 1024 * 1024; #[cfg(feature = "load_file")] /// Size of the file cache in buffers. const CACHE_SIZE: usize = 16; /// The data content of the file. #[derive(Clone)] enum FileData { /// Data content is being streamed from an input stream, and stored in a /// vector of buffers. Streamed { buffers: Arc>> }, #[cfg(feature = "load_file")] /// Data content should be read from a file on disk. File { buffer_cache: Arc>, events: mpsc::Sender, }, #[cfg(feature = "load_file")] /// Data content has been memory mapped. Mapped { mmap: Arc }, #[cfg(feature = "load_file")] /// File is empty. Empty, /// Static content. Static { data: Arc> }, } /// Metadata about a file that is being loaded. struct FileMeta { /// The index of the file. index: FileIndex, /// The loaded file's title. Usually its name. title: String, /// Information about the file. info: RwLock>, /// The length of the file that has been parsed. length: AtomicUsize, /// The offset of each newline in the file. newlines: RwLock>, /// During reload, the number of lines the file had before reloading. reload_old_line_count: RwLock>, /// Set to true when the file has been loaded and parsed. finished: AtomicBool, /// Set to true when the file has been dropped. Checked by background /// threads to exit early. dropped: AtomicBool, /// The most recent error encountered when loading the file. error: RwLock>, /// If needed_lines > newlines.len(), pause loading. needed_lines: AtomicUsize, /// CondVar to wake up file loading. waker: Condvar, /// Mutex used by waker. waker_mutex: Mutex<()>, } #[cfg(feature = "load_file")] /// Event triggered by changes to a file on disk. #[derive(Clone, Copy, Debug)] pub(crate) enum FileEvent { /// File has been appended to. Append, /// File has changed and needs reloading. Reload, } /// Guard to stop reading from a file when it is dropped struct FileGuard { meta: Arc, } impl FileMeta { /// Create new file metadata. fn new(index: FileIndex, title: String) -> FileMeta { FileMeta { index, title, info: RwLock::new(Vec::new()), length: AtomicUsize::new(0usize), newlines: RwLock::new(Vec::new()), reload_old_line_count: RwLock::new(None), finished: AtomicBool::new(false), dropped: AtomicBool::new(false), error: RwLock::new(None), needed_lines: AtomicUsize::new(DEFAULT_NEEDED_LINES), waker: Condvar::new(), waker_mutex: Mutex::new(()), } } } impl FileData { /// Create a new streamed file. /// /// A background thread is started to read from `input` and store the /// content in buffers. Metadata about loading is written to `meta`. /// /// Returns `FileData` containing the buffers that the background thread /// is loading into. fn new_streamed( mut input: impl Read + Send + 'static, meta: Arc, event_sender: EventSender, ) -> FileData { let buffers = Arc::new(RwLock::new(Vec::new())); thread::Builder::new() .name(format!("sp-stream-{}", meta.index)) .spawn({ let buffers = buffers.clone(); move || -> Result<()> { let mut offset = 0usize; let mut total_buffer_size = 0usize; let mut waker_mutex = meta.waker_mutex.lock().unwrap(); loop { // Check if a new buffer must be allocated. if offset == total_buffer_size { let mut buffers = buffers.write().unwrap(); buffers.push(Buffer::new(BUFFER_SIZE)); total_buffer_size += BUFFER_SIZE; } let buffers = buffers.read().unwrap(); let mut write = buffers.last().unwrap().write(); match input.read(&mut write) { Ok(0) => { // The end of the file has been reached. Complete. meta.finished.store(true, Ordering::SeqCst); event_sender.send(Event::Loaded(meta.index))?; return Ok(()); } Ok(len) => { if meta.dropped.load(Ordering::SeqCst) { return Ok(()); } // Some data has been read. Parse its newlines. let line_count = { let mut newlines = meta.newlines.write().unwrap(); for i in 0..len { if write[i] == b'\n' { newlines.push(offset + i); } } // Mark that the data has been written. This // needs to be done here before we drop the // lock for `newlines`. offset += len; write.written(len); meta.length.fetch_add(len, Ordering::SeqCst); newlines.len() }; while line_count >= meta.needed_lines.load(Ordering::SeqCst) { // Enough data is loaded. Pause. waker_mutex = meta.waker.wait(waker_mutex).unwrap(); if meta.dropped.load(Ordering::SeqCst) { return Ok(()); } } } Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {} Err(e) => { let mut error = meta.error.write().unwrap(); *error = Some(e.into()); } } } } }) .unwrap(); FileData::Streamed { buffers } } #[cfg(feature = "load_file")] /// Create a new file from disk. fn new_file>( path: P, meta: Arc, event_sender: EventSender, ) -> Result { let path = path.as_ref(); let mut file = Some(StdFile::open(path)?); let (events, event_rx) = mpsc::channel(); let appending = Arc::new(AtomicBool::new(false)); let buffer_cache = Arc::new(Mutex::new(BufferCache::new(path, BUFFER_SIZE, CACHE_SIZE))); // Create a thread to watch for file change notifications. thread::Builder::new() .name(format!("sp-fchg-{}", meta.index)) .spawn({ let events = events.clone(); let appending = appending.clone(); let meta = meta.clone(); let path = path.to_path_buf(); move || -> Result<()> { loop { let (tx, rx) = mpsc::channel(); let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_millis(500)).expect("create watcher"); watcher .watch(path.clone(), RecursiveMode::NonRecursive) .expect("watch file"); loop { if meta.dropped.load(Ordering::SeqCst) { return Ok(()); } let event = rx.recv(); match event { Ok(DebouncedEvent::NoticeWrite(_)) => { appending.store(true, Ordering::SeqCst); events.send(FileEvent::Append)?; } Ok(DebouncedEvent::Write(_)) => { appending.store(false, Ordering::SeqCst); events.send(FileEvent::Append)?; } Ok(DebouncedEvent::Create(_)) => { events.send(FileEvent::Append)?; } Ok(DebouncedEvent::Rename(_, _)) => { events.send(FileEvent::Reload)?; } Ok(DebouncedEvent::NoticeRemove(_)) | Ok(DebouncedEvent::Chmod(_)) => { events.send(FileEvent::Reload)?; break; } Err(_) => { // The watcher failed for some reason. // Wait before retrying. thread::sleep(Duration::from_secs(1)); break; } _ => {} } } } } }) .unwrap(); // Create a thread to load the file. thread::Builder::new() .name(format!("sp-file-{}", meta.index)) .spawn({ let buffer_cache = buffer_cache.clone(); let path = path.to_path_buf(); move || -> Result<()> { let loaded_instance = UniqueInstance::new(); let appending_instance = UniqueInstance::new(); let reloading_instance = UniqueInstance::new(); let mut total_length = 0; let mut end_data = Vec::new(); loop { meta.length.store(total_length, Ordering::SeqCst); if let Some(mut file) = file.take() { let mut buffer = Vec::new(); buffer.resize(BUFFER_SIZE, 0); loop { match file.read(buffer.as_mut_slice()) { Ok(0) => break, Ok(len) => { if meta.dropped.load(Ordering::SeqCst) { return Ok(()); } let mut newlines = meta.newlines.write().unwrap(); for (i, byte) in buffer.iter().enumerate().take(len) { if *byte == b'\n' { newlines.push(total_length + i); } } total_length += len; meta.length.store(total_length, Ordering::SeqCst); } Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {} Err(e) => { let mut error = meta.error.write().unwrap(); *error = Some(e.into()); } } } // Attempt to read the last 4k of the file. If the file changes, we will // check this portion of the file to see if we need to reload the file. let end_len = total_length.min(4096); end_data.clear(); if file.seek(SeekFrom::End(-(end_len as i64))).is_ok() { end_data.resize(end_len, 0); if let Ok(len) = file.read(end_data.as_mut_slice()) { if len != end_len { end_data.clear(); } } else { end_data.clear(); } } } let (send_event, mut reload) = if appending.load(Ordering::SeqCst) { std::thread::sleep(Duration::from_millis(100)); (false, end_data.is_empty()) } else { meta.finished.store(true, Ordering::SeqCst); event_sender .send_unique(Event::Loaded(meta.index), &loaded_instance)?; { let mut reload_old_line_count = meta.reload_old_line_count.write().unwrap(); *reload_old_line_count = None; } match event_rx.recv() { Ok(FileEvent::Append) => (true, end_data.is_empty()), Ok(FileEvent::Reload) => (true, true), Err(e) => { let mut error = meta.error.write().unwrap(); *error = Some(e.into()); return Ok(()); } } }; match StdFile::open(&path) { Ok(mut f) => { if !reload { let mut new_data = Vec::new(); new_data.resize(end_data.len(), 0); let offset = total_length - end_data.len(); if f.seek(SeekFrom::Start(offset as u64)).is_ok() && f.read(new_data.as_mut_slice()).ok() == Some(end_data.len()) && new_data == end_data { // We can continue where we left off } else { reload = true; } } file = Some(f); } Err(_) => { reload = true; } } if reload { buffer_cache.lock().unwrap().clear(); let mut reload_old_line_count = meta.reload_old_line_count.write().unwrap(); let mut newlines = meta.newlines.write().unwrap(); let count = max( reload_old_line_count.unwrap_or(0), line_count(newlines.as_slice(), total_length), ); *reload_old_line_count = Some(count); newlines.clear(); total_length = 0; if send_event { event_sender.send_unique( Event::Reloading(meta.index), &reloading_instance, )?; } } else if send_event { event_sender .send_unique(Event::Appending(meta.index), &appending_instance)?; } meta.finished.store(false, Ordering::SeqCst); } } }) .unwrap(); Ok(FileData::File { buffer_cache, events, }) } #[cfg(feature = "load_file")] /// Create a new memory mapped file. /// /// The `file` is memory mapped and then a background thread is started to /// parse the newlines in the file. The parsing progress is stored in /// `meta`. /// /// Returns `FileData` containing the memory map. fn new_mapped( file: StdFile, meta: Arc, event_sender: EventSender, ) -> Result { // We can't mmap empty files, so just return an empty filedata if the // file's length is 0. if file.metadata()?.len() == 0 { meta.finished.store(true, Ordering::SeqCst); event_sender.send(Event::Loaded(meta.index))?; return Ok(FileData::Empty); } let mmap = Arc::new(unsafe { Mmap::map(&file)? }); thread::Builder::new() .name(format!("sp-mmap-{}", meta.index)) .spawn({ let mmap = mmap.clone(); move || -> Result<()> { let len = mmap.len(); let blocks = (len + BUFFER_SIZE - 1) / BUFFER_SIZE; for block in 0..blocks { if meta.dropped.load(Ordering::SeqCst) { return Ok(()); } let mut newlines = meta.newlines.write().unwrap(); for i in block * BUFFER_SIZE..min((block + 1) * BUFFER_SIZE, len) { if mmap[i] == b'\n' { newlines.push(i); } } } meta.length.store(len, Ordering::SeqCst); meta.finished.store(true, Ordering::SeqCst); event_sender.send(Event::Loaded(meta.index))?; Ok(()) } }) .unwrap(); Ok(FileData::Mapped { mmap }) } /// Create a new file from static data. /// /// Returns `FileData` containing the static data. fn new_static( data: impl Into>, meta: Arc, event_sender: EventSender, ) -> FileData { let data = Arc::new(data.into()); thread::Builder::new() .name(format!("sp-static-{}", meta.index)) .spawn({ let data = data.clone(); move || -> Result<()> { let len = data.len(); let blocks = (len + BUFFER_SIZE - 1) / BUFFER_SIZE; for block in 0..blocks { if meta.dropped.load(Ordering::SeqCst) { return Ok(()); } let mut newlines = meta.newlines.write().unwrap(); for (i, byte) in data .iter() .enumerate() .skip(block * BUFFER_SIZE) .take(BUFFER_SIZE) { if *byte == b'\n' { newlines.push(i); } } } meta.length.store(len, Ordering::SeqCst); meta.finished.store(true, Ordering::SeqCst); event_sender.send(Event::Loaded(meta.index))?; Ok(()) } }) .unwrap(); FileData::Static { data } } /// Runs the `call` function, passing it a slice of the data from `start` to `end`. /// Tries to avoid copying the data if possible. fn with_slice(&self, start: usize, end: usize, mut call: F) -> T where F: FnMut(Cow<'_, [u8]>) -> T, { match self { FileData::Streamed { buffers } => { let start_buffer = start / BUFFER_SIZE; let end_buffer = (end - 1) / BUFFER_SIZE; let buffers = buffers.read().unwrap(); if start_buffer == end_buffer { let data = buffers[start_buffer].read(); call(Cow::Borrowed( &data[start % BUFFER_SIZE..=(end - 1) % BUFFER_SIZE], )) } else { // The data spans multiple buffers, so we must make a copy to make it contiguous. let mut v = Vec::with_capacity(end - start); v.extend_from_slice(&buffers[start_buffer].read()[start % BUFFER_SIZE..]); for b in start_buffer + 1..end_buffer { v.extend_from_slice(buffers[b].read()); } v.extend_from_slice(&buffers[end_buffer].read()[..=(end - 1) % BUFFER_SIZE]); call(Cow::Owned(v)) } } #[cfg(feature = "load_file")] FileData::File { events, buffer_cache, .. } => { let mut buffer_cache = buffer_cache.lock().unwrap(); buffer_cache .with_slice(start, end, |data| { if data .iter() .take(data.len().saturating_sub(1)) .any(|c| *c == b'\n') { events.send(FileEvent::Reload).unwrap(); } call(data) }) .unwrap() } #[cfg(feature = "load_file")] FileData::Mapped { mmap } => call(Cow::Borrowed(&mmap[start..end])), #[cfg(feature = "load_file")] FileData::Empty => call(Cow::Borrowed(&[])), FileData::Static { data } => call(Cow::Borrowed(&data[start..end])), } } } /// A loaded file. pub(crate) struct LoadedFile { /// The data for the file. data: FileData, /// Metadata about the loading of the file. meta: Arc, /// Guard to stop loading the file when the original reference to it is dropped. _guard: Option, } impl Clone for LoadedFile { fn clone(&self) -> LoadedFile { LoadedFile { data: self.data.clone(), meta: self.meta.clone(), _guard: None, } } } impl LoadedFile { fn new(data: FileData, meta: Arc) -> Self { let _guard = Some(FileGuard { meta: meta.clone() }); LoadedFile { data, meta, _guard } } /// Load stream. pub(crate) fn new_streamed( index: FileIndex, stream: impl Read + Send + 'static, title: &str, event_sender: EventSender, ) -> LoadedFile { let meta = Arc::new(FileMeta::new(index, title.to_string())); let data = FileData::new_streamed(stream, meta.clone(), event_sender); LoadedFile::new(data, meta) } #[cfg(feature = "load_file")] pub(crate) fn new_file( index: FileIndex, filename: &OsStr, event_sender: EventSender, ) -> Result { let title = filename.to_string_lossy().into_owned(); let meta = Arc::new(FileMeta::new(index, title.to_string())); let mut file = StdFile::open(filename).map_err(|err| Error::from(err).with_file(title))?; // Determine whether this file is a real file, or some kind of pipe, by // attempting to do a no-op seek. If it fails, we won't be able to seek // around and load parts of the file at will, so treat it as a stream. let data = match file.seek(SeekFrom::Current(0)) { Ok(_) => FileData::new_file(filename, meta.clone(), event_sender)?, Err(_) => FileData::new_streamed(file, meta.clone(), event_sender), }; Ok(LoadedFile::new(data, meta)) } #[cfg(feature = "load_file")] /// Load a file by memory mapping it if possible. #[allow(unused)] pub(crate) fn new_mapped( index: FileIndex, filename: &OsStr, event_sender: EventSender, ) -> Result { let title = filename.to_string_lossy().into_owned(); let meta = Arc::new(FileMeta::new(index, title.clone())); let mut file = StdFile::open(filename).map_err(|err| Error::from(err).with_file(title))?; // Determine whether this file is a real file, or some kind of pipe, by // attempting to do a no-op seek. If it fails, assume we can't mmap // it. let data = match file.seek(SeekFrom::Current(0)) { Ok(_) => FileData::new_mapped(file, meta.clone(), event_sender)?, Err(_) => FileData::new_streamed(file, meta.clone(), event_sender), }; Ok(LoadedFile::new(data, meta)) } /// Load the output and error of a command pub(crate) fn new_command( index: FileIndex, command: &OsStr, args: I, title: &str, event_sender: EventSender, ) -> Result<(LoadedFile, LoadedFile)> where I: IntoIterator, S: AsRef, { let title_err = format!("STDERR for {}", title); let mut process = Command::new(command) .args(args) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .map_err(|err| Error::from(err).with_command(command))?; let out = process.stdout.take().unwrap(); let err = process.stderr.take().unwrap(); let out_file = LoadedFile::new_streamed(index, out, title, event_sender.clone()); let err_file = LoadedFile::new_streamed(index + 1, err, &title_err, event_sender.clone()); thread::Builder::new() .name(format!("sp-cmd-{}", index)) .spawn({ let out_file = out_file.clone(); move || -> Result<()> { if let Ok(rc) = process.wait() { if !rc.success() { let mut info = out_file.meta.info.write().unwrap(); match rc.code() { Some(code) => info.push(format!("rc: {}", code)), None => info.push("killed!".to_string()), } event_sender.send(Event::RefreshOverlay)?; } } Ok(()) } }) .unwrap(); Ok((out_file, err_file)) } /// Load a file from static data. pub(crate) fn new_static( index: FileIndex, title: &str, data: impl Into>, event_sender: EventSender, ) -> LoadedFile { let meta = Arc::new(FileMeta::new(index, title.to_string())); let data = FileData::new_static(data, meta.clone(), event_sender); LoadedFile::new(data, meta) } } impl FileInfo for LoadedFile { /// The file's index. fn index(&self) -> FileIndex { self.meta.index } /// The file's title. fn title(&self) -> Cow<'_, str> { Cow::Borrowed(&self.meta.title) } /// The file's info. fn info(&self) -> Cow<'_, str> { let info = self.meta.info.read().unwrap(); Cow::Owned(info.join(" ")) } /// True once the file is loaded and all newlines have been parsed. fn loaded(&self) -> bool { self.meta.finished.load(Ordering::SeqCst) } /// Returns the number of lines in the file. fn lines(&self) -> usize { let lines = if !self.meta.finished.load(Ordering::SeqCst) { let reload_old_line_count = self.meta.reload_old_line_count.read().unwrap(); reload_old_line_count.unwrap_or(0) } else { 0 }; let newlines = self.meta.newlines.read().unwrap(); max( lines, line_count(newlines.as_slice(), self.meta.length.load(Ordering::SeqCst)), ) } /// Runs the `call` function, passing it the contents of line `index`. /// Tries to avoid copying the data if possible, however the borrowed /// line only lasts as long as the function call. fn with_line(&self, index: usize, call: F) -> Option where F: FnMut(Cow<'_, [u8]>) -> T, { let newlines = self.meta.newlines.read().unwrap(); if index > newlines.len() { return None; } let start = if index == 0 { 0 } else { newlines[index - 1] + 1 }; let end = if index < newlines.len() { newlines[index] + 1 } else { self.meta.length.load(Ordering::SeqCst) }; if start == end { return None; } Some(self.data.with_slice(start, end, call)) } /// Set how many lines are needed. /// /// If `self.lines()` exceeds that number, pause loading until /// `set_needed_lines` is called with a larger number. /// This is only effective for "streamed" input. fn set_needed_lines(&self, lines: usize) { // This can be simplified by `fetch_max` when it's stable. if self.meta.needed_lines.load(Ordering::SeqCst) >= lines { return; } self.meta.needed_lines.store(lines, Ordering::SeqCst); self.meta.waker.notify_all(); } /// True if the loading thread has been paused. fn paused(&self) -> bool { !self.loaded() && self.meta.waker_mutex.try_lock().is_ok() } } impl Drop for FileGuard { fn drop(&mut self) { self.meta.dropped.store(true, Ordering::SeqCst); // The thread might be blocked. Wake it up so it can notice the change // in `dropped`. self.meta.waker.notify_all(); } } fn line_count(newlines: &[usize], length: usize) -> usize { let mut lines = newlines.len(); let after_last_newline_offset = if lines == 0 { 0 } else { newlines[lines - 1] + 1 }; if length > after_last_newline_offset { lines += 1; } lines } sapling-streampager-0.11.0/src/overstrike.rs000064400000000000000000000220431046102023000172030ustar 00000000000000//! Overstrike Handling //! //! Typewriter-based terminals used to achieve bold and underlined text by //! backspacing over the previous character and then overstriking either a copy //! of the same letter (for bold) or an underscore (for underline). This //! technique is still in use, in particular for man pages. //! //! Handle this by converting runs of overstruck letters into normal text, //! bracketed by the far more modern SGR escape codes. use std::borrow::Cow; use std::str; use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation}; /// An overstrike style. #[derive(Copy, Clone, PartialEq, Eq)] enum Overstrike { Normal, Bold, Underline, BoldUnderline, } impl Overstrike { /// Make the overstrike style bold. fn bold(&mut self) { *self = match *self { Overstrike::Normal | Overstrike::Bold => Overstrike::Bold, Overstrike::Underline | Overstrike::BoldUnderline => Overstrike::BoldUnderline, } } /// Make the overstrike style underlined. fn underline(&mut self) { *self = match *self { Overstrike::Normal | Overstrike::Underline => Overstrike::Underline, Overstrike::Bold | Overstrike::BoldUnderline => Overstrike::BoldUnderline, } } /// Add SGR control sequences to `out` sufficient to switch from the `prev` /// overstrike style to this overstrike style. fn add_control_sequence(self, prev: Overstrike, out: &mut String) { match (prev, self) { (Overstrike::Normal, Overstrike::Bold) => out.push_str("\x1B[1m"), (Overstrike::Normal, Overstrike::Underline) => out.push_str("\x1B[4m"), (Overstrike::Normal, Overstrike::BoldUnderline) => out.push_str("\x1B[1;4m"), (Overstrike::Bold, Overstrike::Normal) => out.push_str("\x1B[22m"), (Overstrike::Bold, Overstrike::Underline) => out.push_str("\x1B[22;4m"), (Overstrike::Bold, Overstrike::BoldUnderline) => out.push_str("\x1B[4m"), (Overstrike::Underline, Overstrike::Normal) => out.push_str("\x1B[24m"), (Overstrike::Underline, Overstrike::Bold) => out.push_str("\x1B[24;1m"), (Overstrike::Underline, Overstrike::BoldUnderline) => out.push_str("\x1B[1m"), (Overstrike::BoldUnderline, Overstrike::Normal) => out.push_str("\x1B[22;24m"), (Overstrike::BoldUnderline, Overstrike::Bold) => out.push_str("\x1B[24m"), (Overstrike::BoldUnderline, Overstrike::Underline) => out.push_str("\x1B[22m"), _ => {} } } } /// Erase the last grapheme from the string. If that's not possible, or if the /// previous grapheme was a control character, add a backspace character to the /// string. fn backspace(out: &mut String) { let mut cursor = GraphemeCursor::new(out.len(), out.len(), true); if let Ok(Some(offset)) = cursor.prev_boundary(out, 0) { if out[offset..] .chars() .next() .map_or(true, char::is_control) { out.push('\x08'); } else { out.truncate(offset); } } else { out.push('\x08'); } } /// Convert a span of unicode characters with overstrikes into a span with /// escape sequences fn convert_unicode_span(input: &str) -> String { let mut result = String::with_capacity(input.len()); let mut prev_grapheme = None; let mut prev_overstrike = Overstrike::Normal; let mut overstrike = Overstrike::Normal; let mut graphemes = input.graphemes(true); while let Some(grapheme) = graphemes.next() { if grapheme == "\x08" { if prev_grapheme.is_some() { if let Some(next_grapheme) = graphemes.next() { if next_grapheme == "\x08" { backspace(&mut result); prev_grapheme = None; overstrike = Overstrike::Normal; } else if prev_grapheme == Some(next_grapheme) { if next_grapheme == "_" { // Overstriking underscore with itself is // ambiguous. Prefer to continue the existing // overstrike if there is any. if overstrike == Overstrike::Normal { if prev_overstrike != Overstrike::Normal { overstrike = prev_overstrike; } else { overstrike.bold(); } } else { overstrike = Overstrike::BoldUnderline; } } else { overstrike.bold() } } else if next_grapheme == "_" { overstrike.underline(); } else if prev_grapheme == Some("_") { overstrike.underline(); prev_grapheme = Some(next_grapheme); } else { overstrike = Overstrike::Normal; prev_grapheme = Some(next_grapheme); } } else { prev_grapheme = None; } } else { backspace(&mut result); overstrike = Overstrike::Normal; } } else { if let Some(prev_grapheme) = prev_grapheme { overstrike.add_control_sequence(prev_overstrike, &mut result); result.push_str(prev_grapheme); } prev_overstrike = overstrike; prev_grapheme = Some(grapheme); overstrike = Overstrike::Normal; } } if let Some(prev_grapheme) = prev_grapheme { overstrike.add_control_sequence(prev_overstrike, &mut result); result.push_str(prev_grapheme); prev_overstrike = overstrike; } Overstrike::Normal.add_control_sequence(prev_overstrike, &mut result); result } /// Convert any overstrike sequences found in the `input` string into normal /// text, bracketed by SGR escape sequences. /// /// For example `"text in b\bbo\bol\bld\bd or l\b_i\b_n\b_e\b_d"` becomes /// `"text in {bold-on}bold{bold-off} or {ul-on}lined{ul-off}"` (where /// `\b` is a backspace and the text in braces is the corresponding SGR /// sequence). pub(crate) fn convert_overstrike(input: &[u8]) -> Cow<'_, [u8]> { if input.contains(&b'\x08') { let mut data = Vec::new(); let mut input = input; loop { match str::from_utf8(input) { Ok(valid) => { data.extend_from_slice(convert_unicode_span(valid).as_bytes()); break; } Err(error) => { let (valid, after_valid) = input.split_at(error.valid_up_to()); if !valid.is_empty() { data.extend_from_slice( convert_unicode_span(unsafe { str::from_utf8_unchecked(valid) }) .as_bytes(), ); } if let Some(len) = error.error_len() { data.extend_from_slice(&after_valid[..len]); input = &after_valid[len..]; } else { data.extend_from_slice(after_valid); break; } } } } Cow::Owned(data) } else { Cow::Borrowed(input) } } #[cfg(test)] mod test { use super::*; #[test] fn test_convert_unicode_span() { // For simplicity, we will use 'B' as backspace in these tests. let bs_re = regex::Regex::new("B").unwrap(); let bs = move |s| bs_re.replace_all(s, "\x08").to_string(); assert_eq!(convert_unicode_span("hello"), "hello"); assert_eq!( convert_unicode_span(&bs("_Bh_Be_Bl_Bl_Bo")), "\x1B[4mhello\x1B[24m" ); assert_eq!( convert_unicode_span(&bs("hBheBelBllBloBo")), "\x1B[1mhello\x1B[22m" ); assert_eq!( convert_unicode_span(&bs( "support bBboBolBldBd, uB_nB__Bd_BérB_lB_íB_nB__Be and bB_BboBoB__BtBthB_BhBh!" )), "support \x1B[1mbold\x1B[22m, \x1B[4mundérlíne\x1B[24m and \x1B[1;4mboth\x1B[22;24m!" ); assert_eq!( convert_unicode_span(&bs("BBxBB can erase bBbBmistayBkes !!BBB.")), bs("BBB can erase mistakes.") ); assert_eq!( convert_unicode_span(&bs("ambig _B_bBb_B_ _B_uB__B_ bBb_B_ uB__B_B_")), "ambig \x1B[1m_b_\x1B[22m \x1B[1m_\x1B[22;4mu_\x1B[24m \x1B[1mb_\x1B[22m \x1B[4mu\x1B[1m_\x1B[22;24m" ); assert_eq!( convert_unicode_span(&bs("combining: a\u{301}Ba bBba\u{301}Ba\u{301}tBt bB_a\u{301}B__Ba\u{301}tB_ xa\u{301}a\u{301}BBx")), "combining: a \x1B[1mba\u{301}t\x1B[22m \x1B[4mba\u{301}a\u{301}t\x1B[24m xx" ); } } sapling-streampager-0.11.0/src/pager.rs000064400000000000000000000235301046102023000161060ustar 00000000000000//! The pager. use std::ffi::OsStr; use std::io::Read; use std::sync::Arc; use termwiz::caps::ColorLevel; use termwiz::caps::{Capabilities, ProbeHints}; use termwiz::terminal::{SystemTerminal, Terminal}; use vec_map::VecMap; use crate::action::ActionSender; use crate::bindings::Keymap; use crate::config::{Config, InterfaceMode, KeymapConfig, WrappingMode}; use crate::control::Controller; use crate::error::{Error, Result}; use crate::event::EventStream; use crate::file::{ControlledFile, File, FileIndex, FileInfo, LoadedFile}; use crate::progress::Progress; /// The main pager state. pub struct Pager { /// The Terminal. term: SystemTerminal, /// The Terminal's capabilites. caps: Capabilities, /// Event Stream to process. events: EventStream, /// Files to load. files: Vec, /// Error file mapping. Maps file indices to the associated error files. error_files: VecMap, /// Progress indicators to display. progress: Option, /// Configuration. config: Config, } /// Determine terminal capabilities. fn termcaps() -> Result { // Get terminal capabilities from the environment, but disable mouse // reporting, as we don't want to change the terminal's mouse handling. // Enable TrueColor support, which is backwards compatible with 16 // or 256 colors. Applications can still limit themselves to 16 or // 256 colors if they want. let hints = ProbeHints::new_from_env() .color_level(Some(ColorLevel::TrueColor)) .mouse_reporting(Some(false)); let caps = Capabilities::new_with_hints(hints).map_err(Error::Termwiz)?; if cfg!(unix) && caps.terminfo_db().is_none() { Err(Error::TerminfoDatabaseMissing) } else { Ok(caps) } } impl Pager { /// Build a `Pager` using the system terminal and config read from the /// config file and environment variables. pub fn new_using_system_terminal() -> Result { Self::new_using_system_terminal_with_config(Config::from_user_config()) } /// Build a `Pager` using the system terminal and the given config pub fn new_using_system_terminal_with_config(config: Config) -> Result { Self::new_with_terminal_func_with_config( move |caps| SystemTerminal::new(caps).map_err(Error::Termwiz), config, ) } /// Build a `Pager` using the system stdio and config read from the config /// file and environment variables. pub fn new_using_stdio() -> Result { Self::new_using_stdio_with_config(Config::from_user_config()) } /// Build a `Pager` using the system stdio and the given config. pub fn new_using_stdio_with_config(config: Config) -> Result { Self::new_with_terminal_func_with_config( move |caps| SystemTerminal::new_from_stdio(caps).map_err(Error::Termwiz), config, ) } #[cfg(unix)] /// Build a `Pager` using the specified terminal input and output and config /// read from the config file and environment variables. pub fn new_with_input_output( input: &impl std::os::unix::io::AsRawFd, output: &impl std::os::unix::io::AsRawFd, ) -> Result { Self::new_with_input_output_with_config(input, output, Config::from_user_config()) } #[cfg(unix)] /// Build a `Pager` using the specified terminal input and output and the /// given config. pub fn new_with_input_output_with_config( input: &impl std::os::unix::io::AsRawFd, output: &impl std::os::unix::io::AsRawFd, config: Config, ) -> Result { Self::new_with_terminal_func_with_config( move |caps| SystemTerminal::new_with(caps, input, output).map_err(Error::Termwiz), config, ) } #[cfg(windows)] /// Build a `Pager` using the specified terminal input and output and config /// read from the config file and environment variables. pub fn new_with_input_output( input: impl std::io::Read + termwiz::istty::IsTty + std::os::windows::io::AsRawHandle, output: impl std::io::Write + termwiz::istty::IsTty + std::os::windows::io::AsRawHandle, ) -> Result { Self::new_with_input_output_with_config(input, output, Config::from_user_config()) } #[cfg(windows)] /// Build a `Pager` using the specified terminal input and output and the given config. pub fn new_with_input_output_with_config( input: impl std::io::Read + termwiz::istty::IsTty + std::os::windows::io::AsRawHandle, output: impl std::io::Write + termwiz::istty::IsTty + std::os::windows::io::AsRawHandle, config: Config, ) -> Result { Self::new_with_terminal_func_with_config( move |caps| SystemTerminal::new_with(caps, input, output).map_err(Error::Termwiz), config, ) } fn new_with_terminal_func_with_config( create_term: impl FnOnce(Capabilities) -> Result, config: Config, ) -> Result { let caps = termcaps()?; let term = create_term(caps.clone())?; let events = EventStream::new(term.waker()); let files = Vec::new(); let error_files = VecMap::new(); let progress = None; Ok(Self { term, caps, events, files, error_files, progress, config, }) } /// Add a stream to be paged. pub fn add_stream( &mut self, stream: impl Read + Send + 'static, title: &str, ) -> Result { let index = self.files.len(); let event_sender = self.events.sender(); let file = LoadedFile::new_streamed(index, stream, title, event_sender); self.files.push(file.into()); Ok(index) } /// Attach an error stream to the previously added output stream. pub fn add_error_stream( &mut self, stream: impl Read + Send + 'static, title: &str, ) -> Result { let index = self.files.len(); let event_sender = self.events.sender(); let file = LoadedFile::new_streamed(index, stream, title, event_sender); if let Some(out_file) = self.files.last() { self.error_files .insert(out_file.index(), file.clone().into()); } self.files.push(file.into()); Ok(index) } #[cfg(feature = "load_file")] /// Attach a file from disk. pub fn add_file(&mut self, filename: &OsStr) -> Result { let index = self.files.len(); let event_sender = self.events.sender(); let file = LoadedFile::new_file(index, filename, event_sender)?; self.files.push(file.into()); Ok(index) } /// Attach a controlled file. pub fn add_controlled_file(&mut self, controller: &Controller) -> Result { let index = self.files.len(); let event_sender = self.events.sender(); let file = ControlledFile::new(controller, index, event_sender); self.files.push(file.into()); Ok(index) } /// Attach the output and error streams from a subprocess. /// /// Returns the file index for each stream. pub fn add_subprocess( &mut self, command: &OsStr, args: I, title: &str, ) -> Result<(FileIndex, FileIndex)> where I: IntoIterator, S: AsRef, { let index = self.files.len(); let event_sender = self.events.sender(); let (out_file, err_file) = LoadedFile::new_command(index, command, args, title, event_sender)?; self.error_files.insert(index, err_file.clone().into()); self.files.push(out_file.into()); self.files.push(err_file.into()); Ok((index, index + 1)) } /// Set the progress stream. pub fn set_progress_stream(&mut self, stream: impl Read + Send + 'static) { let event_sender = self.events.sender(); self.progress = Some(Progress::new(stream, event_sender)); } /// Set when to use full screen mode. See [`InterfaceMode`] for details. pub fn set_interface_mode(&mut self, value: impl Into) { self.config.interface_mode = value.into(); } /// Set whether scrolling can past end of file. pub fn set_scroll_past_eof(&mut self, value: bool) { self.config.scroll_past_eof = value; } /// Set how many lines to read ahead. pub fn set_read_ahead_lines(&mut self, lines: usize) { self.config.read_ahead_lines = lines; } /// Set whether to poll input during start-up (delayed or direct mode). pub fn set_startup_poll_input(&mut self, poll_input: bool) { self.config.startup_poll_input = poll_input; } /// Set whether to show the ruler by default. pub fn set_show_ruler(&mut self, show_ruler: bool) { self.config.show_ruler = show_ruler; } /// Set default wrapping mode. See [`WrappingMode`] for details. pub fn set_wrapping_mode(&mut self, value: impl Into) { self.config.wrapping_mode = value.into(); } /// Set keymap name. pub fn set_keymap_name(&mut self, keymap: impl Into) { self.config.keymap = KeymapConfig::Name(keymap.into()); } /// Set keymap. pub fn set_keymap(&mut self, keymap: Keymap) { self.config.keymap = KeymapConfig::Keymap(Arc::new(keymap)); } /// Create an action sender which can be used to send `Action`s to this pager. pub fn action_sender(&self) -> ActionSender { self.events.action_sender() } /// Run Stream Pager. pub fn run(self) -> Result<()> { crate::display::start( self.term, self.caps, self.events, self.files, self.error_files, self.progress, self.config, ) } } sapling-streampager-0.11.0/src/progress.rs000064400000000000000000000110521046102023000166500ustar 00000000000000//! Progress indicator. //! //! sp can accept another file descriptor from its parent process via the //! `--pager-fd` option or the PAGER_PROGRESS_FD environment variable. This //! should be a pipe on which the parent process sends progress indicator pages. //! //! Progress indicator pages are blocks of text terminated by an ASCII form-feed //! character. The progress indicator will display the most recently received //! page. use std::io::{BufRead, BufReader, Read}; use std::sync::{Arc, RwLock}; use std::thread; use crate::error::Result; use crate::event::{Event, EventSender, UniqueInstance}; /// Initial buffer size for progress indicator pages. const PROGRESS_BUFFER_SIZE: usize = 4096; /// Inner struct for the progress indicator. pub(crate) struct ProgressInner { /// Buffer containing the currently displayed page. buffer: Vec, /// Offsets of all the newlines in the current page. newlines: Vec, /// Whether the progress indicator is finished because the other /// end of the pipe closed. finished: bool, } /// A progress indicator. #[derive(Clone)] pub(crate) struct Progress { /// The inner progress indicator data. inner: Arc>, } impl Progress { /// Create a new progress indicator that receives progress pages on the /// given file descriptor. Progress events are sent on the event_sender /// whenever a new page is received. pub(crate) fn new(reader: impl Read + Send + 'static, event_sender: EventSender) -> Progress { let inner = Arc::new(RwLock::new(ProgressInner { buffer: Vec::new(), newlines: Vec::new(), finished: false, })); let mut input = BufReader::new(reader); thread::Builder::new() .name(String::from("sp-progress")) .spawn({ let inner = inner.clone(); let progress_unique = UniqueInstance::new(); move || -> Result<()> { loop { let mut buffer = Vec::with_capacity(PROGRESS_BUFFER_SIZE); match input.read_until(b'\x0C', &mut buffer) { Ok(0) | Err(_) => { let mut inner = inner.write().unwrap(); inner.buffer = Vec::new(); inner.newlines = Vec::new(); inner.finished = true; return Ok(()); } Ok(len) => { buffer.truncate(len - 1); let mut newlines = Vec::new(); for (i, byte) in buffer.iter().enumerate().take(len - 1) { if *byte == b'\n' { newlines.push(i); } } let mut inner = inner.write().unwrap(); inner.buffer = buffer; inner.newlines = newlines; event_sender.send_unique(Event::Progress, &progress_unique)?; } } } } }) .unwrap(); Progress { inner } } /// Returns the number of lines in the current page. pub(crate) fn lines(&self) -> usize { let inner = self.inner.read().unwrap(); if inner.finished { return 0; } let mut lines = inner.newlines.len(); let after_last_newline_offset = if lines == 0 { 0 } else { inner.newlines[lines - 1] + 1 }; if inner.buffer.len() > after_last_newline_offset { lines += 1; } lines } /// Calls the callback `call` with the given line of the current page. pub(crate) fn with_line(&self, index: usize, mut call: F) -> Option where F: FnMut(&[u8]) -> T, { let inner = self.inner.read().unwrap(); if index > inner.newlines.len() { return None; } let start = if index == 0 { 0 } else { inner.newlines[index - 1] + 1 }; let end = if index < inner.newlines.len() { inner.newlines[index] + 1 } else { inner.buffer.len() }; if start == end { return None; } Some(call(&inner.buffer[start..end])) } } sapling-streampager-0.11.0/src/prompt.rs000064400000000000000000000371321046102023000163340ustar 00000000000000//! Prompts for input. use std::char; use std::fmt::Write; use termwiz::cell::{AttributeChange, CellAttributes}; use termwiz::color::{AnsiColor, ColorAttribute}; use termwiz::input::KeyEvent; use termwiz::surface::change::Change; use termwiz::surface::Position; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use crate::display::DisplayAction; use crate::error::Error; use crate::prompt_history::PromptHistory; use crate::screen::Screen; use crate::util; type PromptRunFn = dyn FnMut(&mut Screen, &str) -> Result; /// A prompt for input from the user. pub(crate) struct Prompt { /// The text of the prompt to display to the user. prompt: String, /// The current prompt history, history: PromptHistory, /// The closure to run when the user presses Return. Will only be called once. run: Option>, } pub(crate) struct PromptState { /// The value the user is typing in. value: Vec, /// The offset within the value that we are displaying from. offset: usize, /// The cursor position within the value. position: usize, } impl PromptState { pub(crate) fn new() -> PromptState { PromptState { value: Vec::new(), offset: 0, position: 0, } } pub(crate) fn load(data: &str) -> PromptState { let mut value = Vec::new(); let mut iter = data.chars(); while let Some(c) = iter.next() { if c == '\\' { if let Some(c) = iter.next() { if c == 'x' { if let (Some(c1), Some(c2)) = (iter.next(), iter.next()) { let hex: String = [c1, c2].iter().collect(); if let Some(c) = u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) { value.push(c); } } } else { value.push(c); } } } else { value.push(c); } } let position = value.len(); PromptState { value, offset: 0, position, } } pub(crate) fn save(&self) -> String { let mut data = String::new(); for &c in self.value.iter() { if c == '\\' { data.push_str("\\\\"); } else if c < ' ' || c == '\x7f' { write!(data, "\\x{:02X}", c as u8).expect("writes to strings can't fail") } else { data.push(c); } } data } /// Returns the column for the cursor. pub(crate) fn cursor_position(&self) -> usize { let mut position = 0; for c in self.value[self.offset..self.position].iter() { position += render_width(*c); } position } /// Clamp the offset to values appropriate for the length of the value and /// the current cursor position. Keeps at least 4 characters visible to the /// left and right of the value if possible. fn clamp_offset(&mut self, width: usize) { if self.offset > self.position { self.offset = self.position; } while self.cursor_position() < 5 && self.offset > 0 { self.offset -= 1; } while self.cursor_position() > width - 5 && self.offset < self.position { self.offset += 1; } } /// Renders the prompt onto the terminal. fn render(&mut self, changes: &mut Vec, mut position: usize, width: usize) { let mut start = self.offset; let mut end = self.offset; while end < self.value.len() { let c = self.value[end]; if let Some(render) = special_render(self.value[end]) { if end > start { let value: String = self.value[start..end].iter().collect(); changes.push(Change::Text(value)); } let render = util::truncate_string(render, 0, width - position); position += render.width(); changes.push(Change::Attribute(AttributeChange::Reverse(true))); changes.push(Change::Text(render)); changes.push(Change::Attribute(AttributeChange::Reverse(false))); start = end + 1; // Control characters can't compose, so stop if we hit the end. if position >= width { break; } } else { let w = c.width().unwrap_or(0); if position + w > width { // This character would take us past the end, so stop. break; } position += w; } end += 1; } if end > start { let value: String = self.value[start..end].iter().collect(); changes.push(Change::Text(value)); } if position < width { changes.push(Change::ClearToEndOfLine(ColorAttribute::default())); } } /// Insert a character at the current position. fn insert_char(&mut self, c: char, width: usize) -> DisplayAction { self.value.insert(self.position, c); self.position += 1; if self.position == self.value.len() && self.cursor_position() < width - 5 { DisplayAction::Change(Change::Text(c.to_string())) } else { DisplayAction::RefreshPrompt } } fn insert_str(&mut self, s: &str) -> DisplayAction { let old_len = self.value.len(); self.value.splice(self.position..self.position, s.chars()); self.position += self.value.len() - old_len; DisplayAction::RefreshPrompt } /// Delete previous character. fn delete_prev_char(&mut self) -> DisplayAction { if self.position > 0 { self.value.remove(self.position - 1); self.position -= 1; DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Delete next character. fn delete_next_char(&mut self) -> DisplayAction { if self.position < self.value.len() { self.value.remove(self.position); DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Delete previous word. fn delete_prev_word(&mut self) -> DisplayAction { let dest = move_word_backwards(self.value.as_slice(), self.position); if dest != self.position { self.value.splice(dest..self.position, None); self.position = dest; DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Delete next word. fn delete_next_word(&mut self) -> DisplayAction { let dest = move_word_forwards(self.value.as_slice(), self.position); if dest != self.position { self.value.splice(self.position..dest, None); DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Move right one character. fn move_next_char(&mut self) -> DisplayAction { if self.position < self.value.len() { self.position += 1; while self.position < self.value.len() { let w = render_width(self.value[self.position]); if w != 0 { break; } self.position += 1; } DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Move left one character. fn move_prev_char(&mut self) -> DisplayAction { if self.position > 0 { while self.position > 0 { self.position -= 1; let w = render_width(self.value[self.position]); if w != 0 { break; } } DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Move right one word. fn move_next_word(&mut self) -> DisplayAction { let dest = move_word_forwards(self.value.as_slice(), self.position); if dest != self.position { self.position = dest; DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Move left one word. fn move_prev_word(&mut self) -> DisplayAction { let dest = move_word_backwards(self.value.as_slice(), self.position); if dest != self.position { self.position = dest; DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Delete to end of line. fn delete_to_end(&mut self) -> DisplayAction { if self.position < self.value.len() { self.value.splice(self.position.., None); DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Delete to start of line. fn delete_to_start(&mut self) -> DisplayAction { if self.position > 0 { self.value.splice(..self.position, None); self.position = 0; DisplayAction::RefreshPrompt } else { DisplayAction::None } } /// Move to end of line. fn move_to_end(&mut self) -> DisplayAction { self.position = self.value.len(); DisplayAction::RefreshPrompt } /// Move to beginning of line. fn move_to_start(&mut self) -> DisplayAction { self.position = 0; DisplayAction::RefreshPrompt } /// Transpose characters. fn transpose_chars(&mut self) -> DisplayAction { if self.position > 0 && self.value.len() > 1 { if self.position < self.value.len() { self.position += 1; } self.value.swap(self.position - 2, self.position - 1); DisplayAction::RefreshPrompt } else { DisplayAction::None } } } impl Prompt { /// Create a new prompt. pub(crate) fn new(ident: impl Into, prompt: &str, run: Box) -> Prompt { Prompt { prompt: prompt.to_string(), history: PromptHistory::open(ident), run: Some(run), } } fn state(&self) -> &PromptState { self.history.state() } fn state_mut(&mut self) -> &mut PromptState { self.history.state_mut() } /// Returns the column for the cursor. pub(crate) fn cursor_position(&self) -> usize { self.prompt.width() + 4 + self.state().cursor_position() } /// Renders the prompt onto the terminal. pub(crate) fn render(&mut self, changes: &mut Vec, row: usize, width: usize) { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(row), }); changes.push(Change::AllAttributes( CellAttributes::default() .set_foreground(AnsiColor::Black) .set_background(AnsiColor::Silver) .clone(), )); changes.push(Change::Text(format!(" {} ", self.prompt))); changes.push(Change::AllAttributes(CellAttributes::default())); changes.push(Change::Text(" ".into())); let offset = self.prompt.width() + 4; self.state_mut().render(changes, offset, width); } /// Dispatch a key press to the prompt. pub(crate) fn dispatch_key(&mut self, key: KeyEvent, width: usize) -> DisplayAction { use termwiz::input::{KeyCode::*, Modifiers}; const CTRL: Modifiers = Modifiers::CTRL; const NONE: Modifiers = Modifiers::NONE; const ALT: Modifiers = Modifiers::ALT; let value_width = width - self.prompt.width() - 4; let action = match (key.modifiers, key.key) { (NONE, Enter) | (CTRL, Char('j')) | (CTRL, Char('m')) => { // Finish. let _ = self.history.save(); let mut run = self.run.take(); let value: String = self.state().value[..].iter().collect(); return DisplayAction::Run(Box::new(move |screen: &mut Screen| { screen.clear_prompt(); if let Some(ref mut run) = run { run(screen, &value) } else { Ok(DisplayAction::Render) } })); } (NONE, Escape) | (CTRL, Char('c')) => { // Cancel. return DisplayAction::Run(Box::new(|screen: &mut Screen| { screen.clear_prompt(); Ok(DisplayAction::Render) })); } (NONE, Char(c)) => self.state_mut().insert_char(c, value_width), (NONE, Backspace) | (CTRL, Char('h')) => self.state_mut().delete_prev_char(), (NONE, Delete) | (CTRL, Char('d')) => self.state_mut().delete_next_char(), (CTRL, Char('w')) | (ALT, Backspace) => self.state_mut().delete_prev_word(), (ALT, Char('d')) => self.state_mut().delete_next_word(), (NONE, RightArrow) | (CTRL, Char('f')) => self.state_mut().move_next_char(), (NONE, LeftArrow) | (CTRL, Char('b')) => self.state_mut().move_prev_char(), (CTRL, RightArrow) | (ALT, Char('f')) => self.state_mut().move_next_word(), (CTRL, LeftArrow) | (ALT, Char('b')) => self.state_mut().move_prev_word(), (CTRL, Char('k')) => self.state_mut().delete_to_end(), (CTRL, Char('u')) => self.state_mut().delete_to_start(), (NONE, End) | (CTRL, Char('e')) => self.state_mut().move_to_end(), (NONE, Home) | (CTRL, Char('a')) => self.state_mut().move_to_start(), (CTRL, Char('t')) => self.state_mut().transpose_chars(), (NONE, UpArrow) => self.history.previous(), (NONE, DownArrow) => self.history.next(), _ => return DisplayAction::None, }; self.state_mut().clamp_offset(value_width); action } /// Paste some text into the prompt. pub(crate) fn paste(&mut self, text: &str, width: usize) -> DisplayAction { let value_width = width - self.prompt.width() - 4; let action = self.state_mut().insert_str(text); self.state_mut().clamp_offset(value_width); action } } fn move_word_forwards(value: &[char], mut position: usize) -> usize { let len = value.len(); while position < len && value[position].is_whitespace() { position += 1; } while position < len && !value[position].is_whitespace() { position += 1; } position } fn move_word_backwards(value: &[char], mut position: usize) -> usize { while position > 0 { position -= 1; if !value[position].is_whitespace() { break; } } while position > 0 { if value[position].is_whitespace() { position += 1; break; } position -= 1; } position } /// Determine the rendering width for a character. fn render_width(c: char) -> usize { if c < ' ' || c == '\x7F' { // Render as 4 } else if let Some(w) = c.width() { // Render as the character itself w } else { // Render as 8 } } /// Determine the special rendering for a character, if any. fn special_render(c: char) -> Option { if c < ' ' || c == '\x7F' { Some(format!("<{:02X}>", c as u8)) } else if c.width().is_none() { Some(format!("", c as u32)) } else { None } } sapling-streampager-0.11.0/src/prompt_history.rs000064400000000000000000000111411046102023000201050ustar 00000000000000//! Prompt History. use std::fs::File; use std::io::{BufRead, BufReader, Write}; use tempfile::NamedTempFile; use crate::display::DisplayAction; use crate::error::Error; use crate::prompt::PromptState; const HISTORY_LENGTH: usize = 1000; struct HistoryEntry { /// The stored state of the history entry. stored: Option, /// The active state of the history entry. state: Option, } impl HistoryEntry { fn new() -> Self { HistoryEntry { stored: None, state: Some(PromptState::new()), } } fn load(data: String) -> Self { HistoryEntry { stored: Some(data), state: None, } } fn save(&self) -> Option { self.state.as_ref().map(|state| state.save()) } fn activate(&mut self) { if self.state.is_none() { if let Some(stored) = &self.stored { self.state = Some(PromptState::load(stored)); } else { self.state = Some(PromptState::new()); } } } fn state(&self) -> &PromptState { self.state.as_ref().expect("state should exist") } fn state_mut(&mut self) -> &mut PromptState { self.state.as_mut().expect("state should exist") } } pub(crate) struct PromptHistory { ident: String, entries: Vec, active_index: usize, } impl PromptHistory { pub(crate) fn open(ident: impl Into) -> Self { let ident = ident.into(); let mut entries = Vec::new(); if let Some(mut path) = dirs::data_dir() { path.push("streampager"); path.push("history"); path.push(format!("{}.history", ident)); if let Ok(file) = File::open(path) { let file = BufReader::new(file); entries = file .lines() .filter_map(|entry| entry.map(HistoryEntry::load).ok()) .collect(); } } let active_index = entries.len(); entries.push(HistoryEntry::new()); PromptHistory { ident, entries, active_index, } } pub(crate) fn state(&self) -> &PromptState { self.entries[self.active_index].state() } pub(crate) fn state_mut(&mut self) -> &mut PromptState { self.entries[self.active_index].state_mut() } /// Stored value from history. pub(crate) fn stored(&self) -> Option { self.entries[self.active_index].stored.clone() } pub(crate) fn previous(&mut self) -> DisplayAction { if self.active_index > 0 { self.active_index -= 1; self.entries[self.active_index].activate(); DisplayAction::RefreshPrompt } else { DisplayAction::None } } pub(crate) fn next(&mut self) -> DisplayAction { if self.active_index < self.entries.len() - 1 { self.active_index += 1; self.entries[self.active_index].activate(); DisplayAction::RefreshPrompt } else { DisplayAction::None } } pub(crate) fn save(&mut self) -> Result<(), Error> { if let Some(data) = self.entries[self.active_index].save() { if data.is_empty() { return Ok(()); } if self.entries.len() > 1 { if let Some(previous_data) = &self.entries[self.entries.len() - 2].stored { if data == *previous_data { return Ok(()); } } } if let Some(mut path) = dirs::data_dir() { path.push("streampager"); path.push("history"); std::fs::create_dir_all(&path)?; let mut new_file = NamedTempFile::new_in(&path)?; path.push(format!("{}.history", &self.ident)); if let Ok(file) = File::open(&path) { let file = BufReader::new(file); for line in file .lines() .skip(self.entries.len().saturating_sub(HISTORY_LENGTH)) { writeln!(new_file, "{}", line?)?; } } writeln!(new_file, "{}", data)?; new_file.persist(&path)?; } } Ok(()) } } /// Peak the last entry from history. pub(crate) fn peek_last(ident: &str) -> Option { let mut history = PromptHistory::open(ident); history.previous(); history.stored() } sapling-streampager-0.11.0/src/refresh.rs000064400000000000000000000065241046102023000164520ustar 00000000000000//! Track screen refresh regions. use std::cmp::{max, min}; use bit_set::BitSet; /// Tracks which parts of the screen need to be refreshed. #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum Refresh { /// Nothing to render. None, /// The rows in the bitset must be rendered. Rows(BitSet), /// The whole screen must be rendered. All, } fn fill_range(b: &mut BitSet, start: usize, end: usize, fill: bool) { if fill { b.extend(start..end); } else { for row in start..end { b.remove(row); } } } impl Refresh { /// Add a range of rows to the rows that must be rendered. pub(crate) fn add_range(&mut self, start: usize, end: usize) { match *self { Refresh::None => { let mut b = BitSet::new(); b.extend(start..end); *self = Refresh::Rows(b); } Refresh::Rows(ref mut b) => { b.extend(start..end); } Refresh::All => {} } } /// Rotate the range of rows between start and end upwards (towards 0). Rows that roll past /// the start are dropped. New rows introduced are filled with the fill value. pub(crate) fn rotate_range_up(&mut self, start: usize, end: usize, step: usize, fill: bool) { match *self { Refresh::All => {} Refresh::None => { if fill { let mut b = BitSet::new(); let mid = max(start, end.saturating_sub(step)); b.extend(mid..end); *self = Refresh::Rows(b); } } Refresh::Rows(ref mut b) => { let mid = max(start, end.saturating_sub(step)); for row in start..mid { if b.contains(row + step) { b.insert(row); } else { b.remove(row); } } fill_range(b, mid, end, fill); } } } /// Rotate the range of rows between start and end downwards (away from 0). Rows that roll /// past the end are dropped. New rows introduced are filled with the fill value. pub(crate) fn rotate_range_down(&mut self, start: usize, end: usize, step: usize, fill: bool) { match *self { Refresh::None => { if fill { let mut b = BitSet::new(); let mid = min(start.saturating_add(step), end); b.extend(start..mid); *self = Refresh::Rows(b); } } Refresh::Rows(ref mut b) => { let mid = min(start.saturating_add(step), end); for row in (mid..end).rev() { if b.contains(row - step) { b.insert(row); } else { b.remove(row); } } fill_range(b, start, mid, fill); } Refresh::All => {} } } /// Does the range contain the given orow pub(crate) fn contains(&self, row: usize) -> bool { match *self { Refresh::None => false, Refresh::Rows(ref b) => b.contains(row), Refresh::All => true, } } } sapling-streampager-0.11.0/src/ruler.rs000064400000000000000000000201171046102023000161370ustar 00000000000000//! The Ruler use std::cmp::{max, min}; use std::fmt::Write; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Instant; use termwiz::surface::change::Change; use unicode_width::UnicodeWidthStr; use crate::bar::{Bar, BarItem, BarString, BarStyle}; use crate::config::WrappingMode; use crate::file::{File, FileInfo}; use crate::util; pub(crate) struct Ruler { position: Arc, loading: Arc, repeat_count: Arc, ruler_bar: Bar, } impl Ruler { pub(crate) fn new(file: File) -> Self { let title = Arc::new(BarString::new(file.title().to_string())); let file_info = Arc::new(FileInformationIndicator::new(file.clone())); let position = Arc::new(PositionIndicator::new(file.clone())); let loading = Arc::new(LoadingIndicator::new(file)); let repeat_count = Arc::new(RepeatCountIndicator::default()); let mut ruler_bar = Bar::new(BarStyle::Normal); ruler_bar.add_left_item(title); ruler_bar.add_right_item(repeat_count.clone()); ruler_bar.add_right_item(file_info); ruler_bar.add_right_item(position.clone()); ruler_bar.add_right_item(loading.clone()); Ruler { position, loading, repeat_count, ruler_bar, } } pub(crate) fn bar(&self) -> &Bar { &self.ruler_bar } pub(crate) fn set_position( &self, top: usize, left: usize, bottom: Option, wrapping_mode: WrappingMode, ) { self.position.top.store(top, Ordering::SeqCst); self.position.left.store(left, Ordering::SeqCst); let (bottom, following_end) = match bottom { Some(bottom) => (bottom, false), None => (0, true), }; self.position.bottom.store(bottom, Ordering::SeqCst); self.position.line_wrapping.store( wrapping_mode == WrappingMode::GraphemeBoundary, Ordering::SeqCst, ); self.position.word_wrapping.store( wrapping_mode == WrappingMode::WordBoundary, Ordering::SeqCst, ); self.loading .following_end .store(following_end, Ordering::SeqCst); } pub(crate) fn set_repeat_count(&self, count: Option) { self.repeat_count .count .store(count.unwrap_or(0), Ordering::Relaxed); } } /// Shows the file's additional information. struct FileInformationIndicator { file: File, } impl FileInformationIndicator { fn new(file: File) -> Self { FileInformationIndicator { file } } } impl BarItem for FileInformationIndicator { fn width(&self) -> usize { self.file.info().width() } fn render(&self, changes: &mut Vec, width: usize) { changes.push(Change::Text(util::truncate_string( self.file.info(), 0, width, ))); } } /// Indicates the current position within the file. struct PositionIndicator { file: File, top: AtomicUsize, left: AtomicUsize, bottom: AtomicUsize, line_wrapping: AtomicBool, word_wrapping: AtomicBool, } impl PositionIndicator { pub(crate) fn new(file: File) -> Self { PositionIndicator { file, top: AtomicUsize::new(0), left: AtomicUsize::new(0), bottom: AtomicUsize::new(0), line_wrapping: AtomicBool::new(false), word_wrapping: AtomicBool::new(false), } } } impl BarItem for PositionIndicator { fn width(&self) -> usize { let top = self.top.load(Ordering::SeqCst); let left = self.left.load(Ordering::SeqCst); let bottom = self.bottom.load(Ordering::SeqCst); let line_wrapping = self.line_wrapping.load(Ordering::SeqCst); let word_wrapping = self.word_wrapping.load(Ordering::SeqCst); let mut width = 0; let file_lines = self.file.lines(); let nw = max(3, util::number_width(max(file_lines, max(bottom, top + 1)))); if line_wrapping || word_wrapping { width += 6; } else if left > 1 { // Indicate horizontal position as "+N" if we are not at the very left. width += util::number_width(left + 1) + 3; } if top > file_lines { // We are past end of the file, show as "line NNN/NNN". width += 2 * nw + 6; } else { // We are displaying normally, show as "lines NNN-NNN/NNN". width += 3 * nw + 8; } width } fn render(&self, changes: &mut Vec, width: usize) { let top = self.top.load(Ordering::SeqCst); let left = self.left.load(Ordering::SeqCst); let bottom = self.bottom.load(Ordering::SeqCst); let line_wrapping = self.line_wrapping.load(Ordering::SeqCst); let word_wrapping = self.word_wrapping.load(Ordering::SeqCst); let file_lines = self.file.lines(); let mut out = String::new(); let nw = max(3, util::number_width(max(file_lines, max(bottom, top + 1)))); if line_wrapping { write!(out, "wrap ").expect("writes to strings should not fail"); } else if word_wrapping { write!(out, "word ").expect("writes to strings should not fail"); } else if left > 0 { write!(out, "{:+} ", left + 1,).expect("writes to strings should not fail"); } if top > file_lines { write!(out, "line {1:0}/{2:0$}", nw, top + 1, file_lines) } else if bottom > 0 { write!( out, "lines {1:0$}-{2:0$}/{3:0$.0$}", nw, top + 1, min(bottom, file_lines), file_lines, ) } else { write!( out, "lines {1:0$}-{2:0$}/{3:0$.0$}", nw, top + 1, "END", file_lines, ) } .expect("writes to strings can't fail"); changes.push(Change::Text(util::truncate_string(&out, 0, width))); } } /// Shows whether or not the file is loading. struct LoadingIndicator { file: File, following_end: AtomicBool, animation_start: Instant, } impl LoadingIndicator { fn new(file: File) -> Self { LoadingIndicator { file, following_end: AtomicBool::new(false), animation_start: Instant::now(), } } fn content(&self) -> Option<&'static str> { if self.file.loaded() { None } else if self.file.paused() && !self.following_end.load(Ordering::SeqCst) { Some("[loading paused]") } else { let frame_index = (self.animation_start.elapsed().subsec_millis() / 200) as usize; let frame = [ "[loading • ]", "[loading • ]", "[loading • ]", "[loading • ]", "[loading • ]", ][frame_index]; Some(frame) } } } impl BarItem for LoadingIndicator { fn width(&self) -> usize { if self.file.loaded() { 0 } else { 16 } } fn render(&self, changes: &mut Vec, width: usize) { if let Some(content) = self.content() { changes.push(Change::Text(util::truncate_string(content, 0, width))); } } } #[derive(Default)] struct RepeatCountIndicator { count: AtomicUsize, } impl BarItem for RepeatCountIndicator { fn width(&self) -> usize { let mut count = self.count.load(Ordering::Relaxed); let mut width = 0; while count > 0 { count /= 10; width += 1; } width } fn render(&self, changes: &mut Vec, width: usize) { let count = self.count.load(Ordering::Relaxed); if count > 0 { let content = format!("{}", count); changes.push(Change::Text(util::truncate_string(content, 0, width))); } } } sapling-streampager-0.11.0/src/screen.rs000064400000000000000000001504351046102023000162740ustar 00000000000000//! A screen displaying a single file. //! //! Some terms are used for specific meanings within this file: //! //! * `line` means a line in the file. //! * `row` means a row on the screen. //! * `height` means a height in rows. //! * `portion` means the portion within a line shown on a single row //! when a line has been wrapped onto multiple rows. //! //! An example of how these might map to the screen is shown below: //! //! ```text //! File Screen //! ==== ====== //! LINE 0 PORTION 0 \ +------------------+__v top_line = 0, top_line_portion = 1 //! LINE 0 PORTION 1 \ | LINE 0 PORTION 1 | ^ //! LINE 0 PORTION 2 | LINE 0 PORTION 2 | | //! LINE 1 | LINE 1 | | //! LINE 2 PORTION 0 \ | LINE 2 PORTION 0 | | height = 8 //! LINE 2 PORTION 1 | LINE 2 PORTION 1 | | //! LINE 3 | LINE 3 | | //! LINE 4 PORTION 0 \ | LINE 4 PORTION 0 |__|___v bottom_line = 4 //! LINE 4 PORTION 1 |<= RULER ========>|__v___ overlay_height = 1 //! +------------------+ ^ //! //! ``` use std::cmp::{max, min}; use std::num::NonZeroUsize; use std::sync::Arc; use termwiz::cell::{CellAttributes, Intensity}; use termwiz::color::{AnsiColor, ColorAttribute}; use termwiz::input::KeyEvent; use termwiz::surface::change::Change; use termwiz::surface::{CursorVisibility, Position}; use crate::action::Action; use crate::bindings::{Binding, Keymap}; use crate::command; use crate::config::{Config, WrappingMode}; use crate::display::Capabilities; use crate::display::DisplayAction; use crate::error::Error; use crate::event::EventSender; use crate::file::{File, FileInfo}; use crate::line::Line; use crate::line_cache::LineCache; use crate::progress::Progress; use crate::prompt::Prompt; use crate::prompt_history; use crate::refresh::Refresh; use crate::ruler::Ruler; use crate::search::{MatchMotion, Search, SearchKind}; use crate::util::number_width; const LINE_CACHE_SIZE: usize = 1000; /// The state of the previous render. #[derive(Clone, Debug, Default)] struct RenderState { /// The number of columns on screen. width: usize, /// The number of rows on screen. height: usize, /// The file line at the top of the screen. top_line: usize, /// The porition of the file line at the top of the screen. top_line_portion: usize, /// The file line at the bottom of the screen. bottom_line: usize, /// The column at the left of the screen. left: usize, /// The height of the overlay. overlay_height: usize, /// The number of lines in the file. file_lines: usize, /// The number of searched lines. searched_lines: usize, /// The number of lines in the error file. error_file_lines: usize, /// The last line portion of the error file. This may be incomplete and needs to be /// re-rendered every time. error_file_last_line_portion: Option<(usize, usize)>, /// The number of rows in the progress indicator. progress_height: usize, /// The number of rows showing the error file. error_file_height: usize, /// The row the ruler was rendered to. ruler_row: Option, /// The row the prompt was rendered to. prompt_row: Option, /// The row the error message was rendered to. error_row: Option, /// The row search status was rendered to. search_row: Option, /// The start and end row of each file line in view. file_line_rows: Vec<(usize, usize)>, } impl RenderState { /// Returns the start and end row of the file line on the screen, if the /// file line is currently visible. fn file_line_rows(&self, file_line_index: usize) -> Option<(usize, usize)> { if file_line_index >= self.top_line && file_line_index < self.bottom_line { self.file_line_rows .get(file_line_index - self.top_line) .cloned() } else { None } } } /// A screen that is displaying a single file. pub(crate) struct Screen { /// The file being displayed. pub(crate) file: File, /// An error file potentially being overlayed. error_file: Option, /// The progress indicator potentially being overlayed. progress: Option, /// The keymap in use. keymap: Arc, /// The current width. width: usize, /// The current height. height: usize, /// The current left-most column when not wrapping left: usize, /// The current top-most line top_line: usize, /// The top-most portion of the top-most line top_line_portion: usize, /// Wrapping mode. wrapping_mode: WrappingMode, /// The state of the previous render. rendered: RenderState, /// Whether line numbers are being displayed. line_numbers: bool, /// Cache of `Line`s to display. line_cache: LineCache, /// Cache of `Line`s for the current search. search_line_cache: LineCache, /// The current error that should be displayed to the user. pub(crate) error: Option, /// The current prompt that the user is entering a response into. prompt: Option, /// The current ongoing search. search: Option, /// The ruler. ruler: Ruler, /// Whether the ruler should be shown. show_ruler: bool, /// Whether we are following the end of the file. If `true`, we will scroll down to the /// end as new input arrives. following_end: bool, /// Scroll to a particular line in the file. pending_absolute_scroll: Option, /// Scroll relative number of rows. pending_relative_scroll: isize, /// Which parts of the screens need to be re-rendered. pending_refresh: Refresh, /// Configuration set by the top-level `Pager`. config: Arc, /// Repeat the next operation for the given times. repeat_count: Option, } impl Screen { /// Create a screen that displays a file. pub(crate) fn new(file: File, config: Arc) -> Result { Ok(Screen { error_file: None, progress: None, keymap: config.keymap.load()?, width: 0, height: 0, left: 0, top_line: 0, top_line_portion: 0, wrapping_mode: config.wrapping_mode, rendered: RenderState::default(), line_numbers: false, line_cache: LineCache::new(NonZeroUsize::new(LINE_CACHE_SIZE).unwrap()), search_line_cache: LineCache::new(NonZeroUsize::new(LINE_CACHE_SIZE).unwrap()), error: None, prompt: None, search: None, ruler: Ruler::new(file.clone()), show_ruler: config.show_ruler, following_end: false, pending_absolute_scroll: None, pending_relative_scroll: 0, pending_refresh: Refresh::None, config, file, repeat_count: None, }) } /// Resize the screen pub(crate) fn resize(&mut self, width: usize, height: usize) { if self.width != width || self.height != height { self.width = width; self.height = height; self.pending_refresh = Refresh::All; } } /// Get the screen width pub(crate) fn width(&self) -> usize { self.width } /// Get the current overlay height pub(crate) fn overlay_height(&self) -> usize { self.rendered.overlay_height } /// Get the screen's keymap pub(crate) fn keymap(&self) -> &Keymap { &self.keymap } /// Renders the part of the screen that has changed. pub(crate) fn render(&mut self, caps: &Capabilities) -> Vec { let mut changes = vec![ // Hide the cursor while we render things. Change::CursorVisibility(CursorVisibility::Hidden), ]; // Set up the render state. let mut render = RenderState { width: self.width, height: self.height, file_lines: self.file.lines(), error_file_lines: self.error_file.as_ref().map_or(0, |f| f.lines()), ..Default::default() }; if let Some(search) = self.search.as_ref() { render.searched_lines = search.searched_lines(); } let mut pending_refresh = self.pending_refresh.clone(); let file_loaded = self.file.loaded(); let file_width = if self.line_numbers { render.width - number_width(render.file_lines) - 2 } else { render.width }; #[derive(Copy, Clone, Debug)] enum RowContent { Empty, FileLinePortions { line: usize, first_portion: usize, rows: usize, }, Blank, Error, Prompt, Search, Ruler, ErrorFileLinePortion(usize, usize), ProgressLine(usize), } let mut row_contents = vec![RowContent::Empty; render.height]; // Assign the lines of the error file to rows (in reverse order). let error_file_line_portions: Vec<_> = (0..render.error_file_lines) .rev() .flat_map(|line_index| { let line = self .error_file .as_ref() .and_then(|f| f.with_line(line_index, |line| Line::new(line_index, line))); if let Some(line) = line { let height = line.height(render.width, WrappingMode::WordBoundary); (0..height) .rev() .map(|portion| (line_index, portion)) .collect() } else { Vec::new() } }) .take(8) .collect(); // Compute where the overlay will go let ruler_height = self.show_ruler as usize; render.progress_height = self.progress.as_ref().map_or(0, |f| f.lines()); render.error_file_height = error_file_line_portions.len(); render.overlay_height = render.progress_height + render.error_file_height + ruler_height + self.search.is_some() as usize + self.prompt.is_some() as usize + self.error.is_some() as usize; if render.overlay_height < render.height { let mut row = render.height - render.progress_height; for progress_line in 0..render.progress_height { row_contents[row + progress_line] = RowContent::ProgressLine(progress_line); } row -= render.error_file_height; render.error_file_last_line_portion = error_file_line_portions.first().cloned(); for (error_file_row, error_file_line_portion) in error_file_line_portions.into_iter().rev().enumerate() { row_contents[row + error_file_row] = RowContent::ErrorFileLinePortion( error_file_line_portion.0, error_file_line_portion.1, ); } if self.show_ruler { row -= 1; row_contents[row] = RowContent::Ruler; render.ruler_row = Some(row); } if self.search.is_some() { row -= 1; row_contents[row] = RowContent::Search; render.search_row = Some(row); } if self.prompt.is_some() { row -= 1; row_contents[row] = RowContent::Prompt; render.prompt_row = Some(row); } if self.error.is_some() { row -= 1; row_contents[row] = RowContent::Error; render.error_row = Some(row); } } else { // The overlay doesn't fit. Only show the prompt (if any). render.overlay_height = self.prompt.is_some() as usize; render.progress_height = 0; render.error_file_height = 0; render.error_file_last_line_portion = None; if self.prompt.is_some() { let prompt_row = render.height.saturating_sub(1); row_contents[prompt_row] = RowContent::Prompt; render.prompt_row = Some(prompt_row); } } let file_view_height = render.height - render.overlay_height; let (end_top_line, end_top_line_portion) = { let mut top_line = render.file_lines; let mut top_line_portion = 0; let mut remaining = file_view_height; while top_line > 0 && remaining > 0 { top_line -= 1; if let Some(line) = self.line_cache.get_or_create(&self.file, top_line, None) { let line_height = line.height(file_width, self.wrapping_mode); if line_height > remaining { top_line_portion = line_height - remaining; break; } remaining -= line_height; } } (top_line, top_line_portion) }; // Scroll to end if self.following_end { // See if this is a small relative downwards scroll let mut relative_scroll = None; if (end_top_line, end_top_line_portion) >= (self.top_line, self.top_line_portion) { let mut scroll_by = 0; let mut scroll_line = self.top_line; let mut scroll_line_portion = self.top_line_portion; while scroll_line < end_top_line { if let Some(line) = self.line_cache.get_or_create(&self.file, scroll_line, None) { let line_height = line.height(file_width, self.wrapping_mode); scroll_by += line_height.saturating_sub(scroll_line_portion); if scroll_by > file_view_height { // We've scrolled an entire screen, just jump straight to the end. break; } } scroll_line += 1; scroll_line_portion = 0; } if scroll_line == end_top_line { scroll_by += end_top_line_portion.saturating_sub(scroll_line_portion); relative_scroll = Some(scroll_by); } } if let Some(relative_scroll) = relative_scroll { self.pending_relative_scroll = relative_scroll as isize; } else { self.top_line = end_top_line; self.top_line_portion = end_top_line_portion; pending_refresh.add_range(0, file_view_height); } } // Perform pending absolute scroll if let Some(line) = self.pending_absolute_scroll.take() { self.top_line = line; self.top_line_portion = 0; pending_refresh.add_range(0, file_view_height); // Scroll up so that the target line is in the center of the // file view. self.pending_relative_scroll -= (file_view_height / 2) as isize; } enum Direction { None, Up, Down, } // Perform pending relative scroll let mut scroll_direction = Direction::None; let mut scroll_distance = 0; if self.pending_relative_scroll < 0 { scroll_direction = Direction::Up; let mut scroll_up = (-self.pending_relative_scroll) as usize; let mut top_line = self.top_line; let mut top_line_portion = self.top_line_portion; if top_line_portion > 0 { let top_line_remaining = min(top_line_portion, scroll_up); top_line_portion -= top_line_remaining; scroll_up -= top_line_remaining; scroll_distance += top_line_remaining; } while scroll_up > 0 && top_line > 0 { top_line -= 1; top_line_portion = 0; if let Some(line) = self.line_cache.get_or_create(&self.file, top_line, None) { let line_height = line.height(file_width, self.wrapping_mode); if line_height > scroll_up { scroll_distance += scroll_up; top_line_portion = line_height - scroll_up; break; } scroll_distance += line_height; scroll_up -= line_height; } } self.top_line = top_line; self.top_line_portion = top_line_portion; } else if self.pending_relative_scroll > 0 { scroll_direction = Direction::Down; let mut scroll_down = self.pending_relative_scroll as usize; let mut top_line = self.top_line; let mut top_line_portion = self.top_line_portion; let (max_top_line, max_top_line_portion) = if self.config.scroll_past_eof { let last_line = render.file_lines.saturating_sub(1); let line_height = if let Some(line) = self.line_cache.get_or_create(&self.file, last_line, None) { line.height(file_width, self.wrapping_mode) } else { 1 }; (last_line, line_height.saturating_sub(1)) } else { (end_top_line, end_top_line_portion) }; while scroll_down > 0 && (top_line, top_line_portion) < (max_top_line, max_top_line_portion) { if let Some(line) = self.line_cache.get_or_create(&self.file, top_line, None) { let line_height = line.height(file_width, self.wrapping_mode); let line_height_remaining = line_height.saturating_sub(top_line_portion); if line_height_remaining > scroll_down { scroll_distance += scroll_down; top_line_portion += scroll_down; break; } scroll_distance += line_height_remaining; scroll_down -= line_height_remaining; } top_line += 1; top_line_portion = 0; } self.top_line = top_line; self.top_line_portion = top_line_portion; } render.top_line = self.top_line; render.top_line_portion = self.top_line_portion; render.left = self.left; self.pending_relative_scroll = 0; // Scroll the region of the screen that had and still has file lines if pending_refresh != Refresh::All { let scroll_start = 0; let scroll_end = min( file_view_height, self.rendered.height - self.rendered.overlay_height, ); match scroll_direction { Direction::None => {} _ if scroll_distance > scroll_end - scroll_start => { pending_refresh.add_range(scroll_start, scroll_end); } Direction::Up if caps.scroll_up => { changes.push(Change::ScrollRegionDown { first_row: scroll_start, region_size: scroll_end - scroll_start, scroll_count: scroll_distance, }); pending_refresh.rotate_range_down( scroll_start, scroll_end, scroll_distance, true, ); } Direction::Down if caps.scroll_down => { changes.push(Change::ScrollRegionUp { first_row: scroll_start, region_size: scroll_end - scroll_start, scroll_count: scroll_distance, }); pending_refresh.rotate_range_up( scroll_start, scroll_end, scroll_distance, true, ); } _ if scroll_distance > 0 => { pending_refresh.add_range(scroll_start, scroll_end); } _ => {} } if file_view_height > scroll_end { pending_refresh.add_range(scroll_end, file_view_height); } } // Assign lines to the rows on screen { let mut file_line_rows = Vec::new(); let mut row = 0; let mut top_portion = render.top_line_portion; for file_line in render.top_line..render.file_lines { if let Some(line) = self.line_cache.get_or_create(&self.file, file_line, None) { let line_height = line.height(file_width, self.wrapping_mode); let visible_line_height = min( line_height.saturating_sub(top_portion), file_view_height - row, ); for offset in 0..visible_line_height { row_contents[row + offset] = RowContent::FileLinePortions { line: file_line, first_portion: top_portion + offset, rows: 1, }; } file_line_rows.push((row, row + visible_line_height)); row += visible_line_height; } else { file_line_rows.push((row, row)); } top_portion = 0; if row >= file_view_height { break; } } render.bottom_line = render.top_line + file_line_rows.len(); render.file_line_rows = file_line_rows; for blank_row in row_contents.iter_mut().take(file_view_height).skip(row) { *blank_row = RowContent::Blank; } } // Update the ruler with the new position. self.ruler.set_position( render.top_line, render.left, if !self.following_end { Some(render.bottom_line) } else { None }, self.wrapping_mode, ); // Work out what else needs to be refreshed if pending_refresh != Refresh::All { // What needs to be refreshed because more of the file was loaded? if !file_loaded { let last_line = self.rendered.file_lines.saturating_sub(1); if let Some((start, end)) = render.file_line_rows(last_line) { pending_refresh.add_range(start, end); } } if render.file_lines > self.rendered.file_lines { let start_line = max(self.rendered.file_lines, render.top_line); let end_line = min(render.file_lines, render.bottom_line); for file_line in start_line..end_line { if let Some((start, end)) = render.file_line_rows(file_line) { pending_refresh.add_range(start, end); } } } // What needs to be refreshed because search has progressed? if let Some(search) = self.search.as_ref() { if render.searched_lines > self.rendered.searched_lines { let start_line = max(render.top_line, self.rendered.searched_lines); let end_line = min(render.bottom_line, render.searched_lines); for line in search.matching_lines(start_line, end_line).into_iter() { if let Some((start_row, end_row)) = render.file_line_rows(line) { pending_refresh.add_range(start_row, end_row); } } } } // What needs to be refreshed because the overlay got smaller? if file_view_height > self.rendered.height - self.rendered.overlay_height { pending_refresh.add_range( self.rendered.height - self.rendered.overlay_height, file_view_height, ); } // Which parts of the error file need to be refreshed because they moved? let bottom_row = render.height - render.progress_height; if !file_loaded && self.rendered.error_file_lines > 0 { pending_refresh.add_range(bottom_row - 1, bottom_row); } if self.rendered.error_file_lines != render.error_file_lines || self.rendered.progress_height != render.progress_height || self.rendered.error_file_last_line_portion != render.error_file_last_line_portion { pending_refresh.add_range(bottom_row - render.error_file_height, bottom_row); } // Did the ruler move or does it need updating? if let Some(ruler_row) = render.ruler_row { if self.rendered.ruler_row != Some(ruler_row) || render.top_line != self.rendered.top_line || render.bottom_line != self.rendered.bottom_line || render.left != self.rendered.left { pending_refresh.add_range(ruler_row, ruler_row + 1); } } // Did the prompt move? if let Some(prompt_row) = render.prompt_row { if self.rendered.prompt_row != Some(prompt_row) { pending_refresh.add_range(prompt_row, prompt_row + 1); } } // Did the error message move? if let Some(error_row) = render.error_row { if self.rendered.error_row != Some(error_row) { pending_refresh.add_range(error_row, error_row + 1); } } } if self.wrapping_mode == WrappingMode::GraphemeBoundary && !self.line_numbers { // In wrapped mode with line numbers off, render full lines at once // so that the terminal can handle wrapped lines properly. let mut first_row: Option<(usize, &mut RowContent)> = None; for (row, row_content) in row_contents.iter_mut().enumerate() { match row_content { RowContent::FileLinePortions { line: this_line, first_portion: this_portion, rows: _, } => { match first_row { Some(( first_row, &mut RowContent::FileLinePortions { line, first_portion, ref mut rows, }, )) if *this_line == line && *this_portion == first_portion + *rows => { *rows += 1; *row_content = RowContent::Empty; if pending_refresh.contains(row) { pending_refresh.add_range(first_row, first_row + 1); } continue; } _ => {} } first_row = Some((row, row_content)); } _ => { first_row = None; } } } } // Render pending rows for (row, row_content) in row_contents.into_iter().enumerate() { if pending_refresh.contains(row) { match row_content { RowContent::Empty => {} RowContent::FileLinePortions { line, first_portion, rows, } => { self.render_file_line( &mut changes, row, line, first_portion, rows, render.left, render.width, ); } RowContent::Blank => { self.render_blank_line(&mut changes, row); } RowContent::Error => { self.render_error(&mut changes, row, render.width); } RowContent::Prompt => { self.prompt .as_mut() .expect("prompt should be visible") .render(&mut changes, row, render.width); } RowContent::Search => { if let Some(search) = self.search.as_mut() { search.render(&mut changes, row, render.width); } } RowContent::Ruler => { self.ruler.bar().render(&mut changes, row, render.width); } RowContent::ErrorFileLinePortion(line, portion) => { self.render_error_file_line(&mut changes, row, line, portion, render.width); } RowContent::ProgressLine(line) => { self.render_progress_line(&mut changes, row, line, render.width); } } } } // Set the cursor to the right position and shape. if let Some(prompt) = self.prompt.as_ref() { changes.push(Change::CursorPosition { x: Position::Absolute(prompt.cursor_position()), y: Position::Absolute( render .prompt_row .expect("prompt row should have been calculated"), ), }); changes.push(Change::CursorVisibility(CursorVisibility::Visible)); } else { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Relative(0), }); if self.config.show_cursor { changes.push(Change::CursorVisibility(CursorVisibility::Visible)); } } // Restore attributes to default. changes.push(Change::AllAttributes(CellAttributes::default())); // Record what we've rendered. self.rendered = render; self.pending_refresh = Refresh::None; changes } /// Renders a line of the file on the screen. fn render_file_line( &mut self, changes: &mut Vec, row: usize, line_index: usize, first_portion: usize, rows: usize, left: usize, width: usize, ) { let line = match self.search { Some(ref search) if search.line_matches(line_index) => self .search_line_cache .get_or_create(&self.file, line_index, Some(search.regex())), _ => self.line_cache.get_or_create(&self.file, line_index, None), }; let match_index = self .search .as_ref() .and_then(|search| search.current_match()) .and_then(|(match_line_index, match_index)| { if match_line_index == line_index { Some(match_index) } else { None } }); if let Some(line) = line { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(row), }); changes.push(Change::AllAttributes(CellAttributes::default())); let start = left; let mut end = left.saturating_add(width); if self.line_numbers { let lw = number_width(self.file.lines()); if lw + 2 < width { changes.push(Change::AllAttributes( CellAttributes::default() .set_foreground(AnsiColor::Black) .set_background(AnsiColor::Silver) .clone(), )); if first_portion == 0 { changes.push(Change::Text(format!(" {:>1$} ", line_index + 1, lw))); } else { changes.push(Change::Text(" ".repeat(lw + 2))); }; changes.push(Change::AllAttributes(CellAttributes::default())); end -= lw + 2; } } if self.wrapping_mode == WrappingMode::Unwrapped { line.render(changes, start, end, match_index); } else { line.render_wrapped( changes, first_portion, rows, end - start, self.wrapping_mode, match_index, ); } } else { self.render_blank_line(changes, row); } } fn render_blank_line(&self, changes: &mut Vec, row: usize) { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(row), }); changes.push(Change::AllAttributes(CellAttributes::default())); changes.push(Change::AllAttributes( CellAttributes::default() .set_foreground(AnsiColor::Navy) .set_intensity(Intensity::Bold) .clone(), )); changes.push(Change::Text("~".into())); changes.push(Change::ClearToEndOfLine(ColorAttribute::default())); } fn render_error_file_line( &mut self, changes: &mut Vec, row: usize, line_index: usize, portion: usize, width: usize, ) { if let Some(error_file) = self.error_file.as_ref() { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(row), }); changes.push(Change::AllAttributes(CellAttributes::default())); if let Some(line) = error_file.with_line(line_index, |line| Line::new(line_index, line)) { line.render_wrapped(changes, portion, 1, width, WrappingMode::WordBoundary, None); } else { changes.push(Change::ClearToEndOfLine(ColorAttribute::default())); } } } fn render_progress_line( &mut self, changes: &mut Vec, row: usize, line_index: usize, width: usize, ) { if let Some(progress) = self.progress.as_ref() { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(row), }); changes.push(Change::AllAttributes(CellAttributes::default())); if let Some(line) = progress.with_line(line_index, |line| Line::new(line_index, line)) { line.render(changes, 0, width, None); } else { changes.push(Change::ClearToEndOfLine(ColorAttribute::default())); } } } /// Renders the error message at the bottom of the screen. fn render_error(&mut self, changes: &mut Vec, row: usize, _width: usize) { if let Some(error) = self.error.as_ref() { changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(row), }); changes.push(Change::AllAttributes( CellAttributes::default() .set_foreground(AnsiColor::Black) .set_background(AnsiColor::Maroon) .clone(), )); // TODO: truncate at width changes.push(Change::Text(format!(" {} ", error))); changes.push(Change::AllAttributes(CellAttributes::default())); changes.push(Change::ClearToEndOfLine(ColorAttribute::default())); } } /// Refreshes the ruler on the next render. pub(crate) fn refresh_ruler(&mut self) { if let Some(ruler_row) = self.rendered.ruler_row { self.pending_refresh.add_range(ruler_row, ruler_row + 1); } } /// Refreshes the search bar on the next render. pub(crate) fn refresh_search_status(&mut self) { if let Some(search_row) = self.rendered.search_row { self.pending_refresh.add_range(search_row, search_row + 1); } } /// Refreshes the prompt on the next render. pub(crate) fn refresh_prompt(&mut self) { if let Some(prompt_row) = self.rendered.prompt_row { self.pending_refresh.add_range(prompt_row, prompt_row + 1); } } /// Refreshes the overlay on the next render. pub(crate) fn refresh_overlay(&mut self) { let start = self .rendered .height .saturating_sub(self.rendered.overlay_height); let end = self.rendered.height; self.pending_refresh.add_range(start, end); } /// Refreshes the progress section on the next render. pub(crate) fn refresh_progress(&mut self) { let start = self .rendered .height .saturating_sub(self.rendered.progress_height); let end = self.height; self.pending_refresh.add_range(start, end); } /// Refresh a file line. pub(crate) fn refresh_file_line(&mut self, file_line_index: usize) { if let Some((start_row, end_row)) = self.rendered.file_line_rows(file_line_index) { self.pending_refresh.add_range(start_row, end_row); } } /// Refresh the line with the current match (if any). pub(crate) fn refresh_matched_line(&mut self) { if let Some(ref search) = self.search { if let Some((line_index, _match_index)) = search.current_match() { self.refresh_file_line(line_index); } } } /// Refresh all lines with any matches. pub(crate) fn refresh_matched_lines(&mut self) { if let Some(ref search) = self.search { for line in search .matching_lines(self.rendered.top_line, self.rendered.bottom_line) .into_iter() { self.refresh_file_line(line); } } } /// Triggers a full refresh on the next render. pub(crate) fn refresh(&mut self) { self.pending_refresh = Refresh::All; } /// Scrolls to the given line number. pub(crate) fn scroll_to(&mut self, line: usize) { self.pending_absolute_scroll = Some(line); self.pending_relative_scroll = 0; self.following_end = false; } /// Scroll the screen `step` characters up. fn scroll_up(&mut self, step: usize) { self.pending_relative_scroll -= step as isize; self.following_end = false; } /// Scroll the screen `step` characters down. fn scroll_down(&mut self, step: usize) { self.pending_relative_scroll += step as isize; self.following_end = false; } /// Scroll the screen `step` characters to the left. fn scroll_left(&mut self, step: usize) { if self.wrapping_mode == WrappingMode::Unwrapped && self.left > 0 && step > 0 { self.left = self.left.saturating_sub(step); self.refresh(); } } /// Scroll the screen `step` characters to the right. fn scroll_right(&mut self, step: usize) { if self.wrapping_mode == WrappingMode::Unwrapped && step != 0 { self.left = self.left.saturating_add(step); self.refresh(); } } /// Scroll up (screen / n) * repeat lines. fn scroll_up_screen_fraction(&mut self, n: usize, repeat: usize) { if n != 0 { let lines = (self.rendered.height - self.rendered.overlay_height) / n; self.scroll_up(lines.saturating_mul(repeat)); } } /// Scroll down (screen / n) * repeat lines. fn scroll_down_screen_fraction(&mut self, n: usize, repeat: usize) { if n != 0 { let lines = (self.rendered.height - self.rendered.overlay_height) / n; self.scroll_down(lines.saturating_mul(repeat)); } } /// Scroll left (screen / n) * repeat columns. fn scroll_left_screen_fraction(&mut self, n: usize, repeat: usize) { if n != 0 { let columns = self.rendered.width / n; self.scroll_left(columns.saturating_mul(repeat)); } } /// Scroll right (screen / n) * repeat columns. fn scroll_right_screen_fraction(&mut self, n: usize, repeat: usize) { if n != 0 { let columns = self.rendered.width / n; self.scroll_right(columns.saturating_mul(repeat)); } } /// Dispatch an action to navigate the displayed file. pub(crate) fn dispatch_action( &mut self, action: Action, event_sender: &EventSender, ) -> DisplayAction { use Action::*; match action { Quit => return DisplayAction::Quit, Refresh => return DisplayAction::Refresh, Help => return DisplayAction::ShowHelp, Cancel => { if self.repeat_count.is_some() { self.clear_repeat_count(); } else { self.error_file = None; self.set_search(None); self.error = None; self.refresh(); return DisplayAction::ClearOverlay; } } PreviousFile => return DisplayAction::PreviousFile, NextFile => return DisplayAction::NextFile, ToggleRuler => { self.show_ruler = !self.show_ruler; } ScrollUpLines(n) => { let n = self.apply_repeat_count(n); self.scroll_up(n) } ScrollDownLines(n) => { let n = self.apply_repeat_count(n); self.scroll_down(n) } ScrollUpScreenFraction(n) => { let repeat = self.apply_repeat_count(1); self.scroll_up_screen_fraction(n, repeat) } ScrollDownScreenFraction(n) => { let repeat = self.apply_repeat_count(1); self.scroll_down_screen_fraction(n, repeat) } ScrollToTop | ScrollToBottom if self.repeat_count.is_some() => { if let Some(n) = self.repeat_count { // Convert 1-based to 0-based line number. self.scroll_to(n.max(1) - 1); } } ScrollToTop => self.scroll_to(0), ScrollToBottom => self.following_end = true, ScrollLeftColumns(n) => { let n = self.apply_repeat_count(n); self.scroll_left(n) } ScrollRightColumns(n) => { let n = self.apply_repeat_count(n); self.scroll_right(n) } ScrollLeftScreenFraction(n) => { let repeat = self.apply_repeat_count(1); self.scroll_left_screen_fraction(n, repeat) } ScrollRightScreenFraction(n) => { let repeat = self.apply_repeat_count(1); self.scroll_right_screen_fraction(n, repeat) } ToggleLineNumbers => { self.line_numbers = !self.line_numbers; return DisplayAction::Refresh; } ToggleLineWrapping => { self.wrapping_mode = self.wrapping_mode.next_mode(); return DisplayAction::Refresh; } PromptGoToLine => self.prompt = Some(command::goto()), PromptSearchFromStart => { self.prompt = Some(command::search(SearchKind::First, event_sender.clone())) } PromptSearchForwards => { self.prompt = Some(command::search( SearchKind::FirstAfter(self.rendered.top_line), event_sender.clone(), )) } PromptSearchBackwards => { self.prompt = Some(command::search( SearchKind::FirstBefore(self.rendered.bottom_line), event_sender.clone(), )) } PreviousMatch => self.create_or_move_match(MatchMotion::Previous, event_sender.clone()), NextMatch => self.create_or_move_match(MatchMotion::Next, event_sender.clone()), PreviousMatchLine => { self.create_or_move_match(MatchMotion::PreviousLine, event_sender.clone()) } NextMatchLine => self.create_or_move_match(MatchMotion::NextLine, event_sender.clone()), PreviousMatchScreen => { self.create_or_move_match(MatchMotion::PreviousScreen, event_sender.clone()) } NextMatchScreen => { self.create_or_move_match(MatchMotion::NextScreen, event_sender.clone()) } FirstMatch => self.create_or_move_match(MatchMotion::First, event_sender.clone()), LastMatch => self.create_or_move_match(MatchMotion::Last, event_sender.clone()), AppendDigitToRepeatCount(n) => self.append_digit_to_repeat_count(n), } if !matches!(action, AppendDigitToRepeatCount(_)) { self.clear_repeat_count(); } DisplayAction::Render } /// Dispatch a keypress to navigate the displayed file. pub(crate) fn dispatch_key( &mut self, key: KeyEvent, event_sender: &EventSender, ) -> DisplayAction { if let Some(binding) = self.keymap.get(key.modifiers, key.key) { match binding { Binding::Action(action) => { let action = action.clone(); return self.dispatch_action(action, event_sender); } Binding::Custom(b) => b.run(self.file.index()), Binding::Unrecognized(_) => {} } } DisplayAction::Render } /// Append a digit to the repeat count. pub(crate) fn append_digit_to_repeat_count(&mut self, digit: usize) { assert!(digit < 10); let new_count = match self.repeat_count { None if digit > 0 => Some(digit), None => None, Some(count) => Some(count.saturating_mul(10).saturating_add(digit)), }; self.ruler.set_repeat_count(new_count); self.refresh_ruler(); self.repeat_count = new_count; } /// Clear the repeat count. pub(crate) fn clear_repeat_count(&mut self) { self.ruler.set_repeat_count(None); self.refresh_ruler(); self.repeat_count = None; } /// Multiply `n` by the repeat count. pub(crate) fn apply_repeat_count(&self, n: usize) -> usize { self.repeat_count.unwrap_or(1).saturating_mul(n) } /// Set the search for this file. pub(crate) fn set_search(&mut self, search: Option) { self.search = search; self.search_line_cache.clear(); } /// Set the error file for this file. pub(crate) fn set_error_file(&mut self, error_file: Option) { self.error_file = error_file; } /// Set the progress indicator for this file. pub(crate) fn set_progress(&mut self, progress: Option) { self.progress = progress; } /// Returns true if this screen is currently animating for any reason. pub(crate) fn animate(&self) -> bool { self.error_file.is_some() || (!self.file.loaded() && !self.file.paused()) || self.following_end || self .search .as_ref() .map_or(false, |search| !search.finished()) } /// Dispatch an animation timeout, updating for the next animation frame. pub(crate) fn dispatch_animation(&mut self) -> DisplayAction { if !self.file.loaded() { self.refresh_ruler(); } if self .search .as_ref() .map_or(false, |search| !search.finished()) { self.refresh_overlay(); } if let Some(ref error_file) = self.error_file { if error_file.lines() != self.rendered.error_file_lines { self.refresh_overlay(); } } match &self.pending_refresh { Refresh::None => DisplayAction::None, _ => DisplayAction::Render, } } pub(crate) fn prompt(&mut self) -> &mut Option { &mut self.prompt } /// Clears the prompt from the screen. pub(crate) fn clear_prompt(&mut self) { // Refresh the prompt before we remove it, so that we know which line to refresh. self.refresh_prompt(); self.prompt = None; } /// Called when a search finds its first match in order to scroll to that match. pub(crate) fn search_first_match(&mut self) -> DisplayAction { let current_match = self .search .as_ref() .and_then(|search| search.current_match()); if let Some((line_index, _match_index)) = current_match { self.scroll_to(line_index); self.refresh_matched_lines(); self.refresh_overlay(); return DisplayAction::Render; } DisplayAction::None } /// Called when a search completes. #[allow(clippy::unnecessary_wraps)] pub(crate) fn search_finished(&mut self) -> DisplayAction { self.refresh_matched_lines(); self.refresh_overlay(); DisplayAction::Render } /// Move the currently selected match to a new match. pub(crate) fn move_match(&mut self, motion: MatchMotion) { self.refresh_matched_line(); if let Some(ref mut search) = self.search { let scope = self.rendered.top_line..=self.rendered.bottom_line; search.move_match(motion, scope); if let Some((line_index, _match_index)) = search.current_match() { self.scroll_to(line_index); } self.refresh_matched_line(); self.refresh_search_status(); } } /// Like `move_match`, but create a new search from history based on the /// last pattern on demand. pub(crate) fn create_or_move_match(&mut self, motion: MatchMotion, event_sender: EventSender) { if self.search.is_some() { self.move_match(motion) } else { // Attempt to load search from history. if let Some(pattern) = prompt_history::peek_last("search") { if !pattern.is_empty() { let kind = match motion { MatchMotion::First => SearchKind::First, MatchMotion::Last => SearchKind::FirstBefore(self.file.lines()), MatchMotion::Next | MatchMotion::NextLine | MatchMotion::NextScreen => { SearchKind::FirstAfter(self.rendered.top_line) } MatchMotion::Previous | MatchMotion::PreviousLine | MatchMotion::PreviousScreen => { SearchKind::FirstBefore(self.rendered.bottom_line) } }; if let Ok(search) = Search::new(&self.file, &pattern, kind, event_sender) { self.search = Some(search); self.move_match(motion) } } } } } pub(crate) fn flush_line_caches(&mut self) { self.line_cache.clear(); self.search_line_cache.clear(); } /// Load more lines from a stream. pub(crate) fn maybe_load_more(&mut self) { // Fetch 1 screen + config.read_ahead_lines. let needed_lines = self.rendered.bottom_line + self.height + self.config.read_ahead_lines; self.file.set_needed_lines(needed_lines); } } sapling-streampager-0.11.0/src/search.rs000064400000000000000000000370741046102023000162650ustar 00000000000000//! Searching. use std::borrow::Cow; use std::cmp::min; use std::ops::RangeInclusive; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, RwLock}; use std::thread; use std::time; use bit_set::BitSet; use lazy_static::lazy_static; use regex::bytes::{NoExpand, Regex}; use termwiz::cell::CellAttributes; use termwiz::color::AnsiColor; use termwiz::surface::change::Change; use termwiz::surface::Position; use unicode_width::UnicodeWidthStr; use crate::error::Error; use crate::event::{Event, EventSender}; use crate::file::{File, FileInfo}; use crate::overstrike; const SEARCH_BATCH_SIZE: usize = 10000; lazy_static! { /// Regex for detecting and removing escape sequences during search. pub(crate) static ref ESCAPE_SEQUENCE: Regex = Regex::new("\x1B\\[[0123456789:;\\[?!\"'#%()*+ ]{0,32}m").unwrap(); } /// What kind of search to perform. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) enum SearchKind { First, FirstAfter(usize), FirstBefore(usize), } /// Motion when changing search matches. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) enum MatchMotion { First, Previous, PreviousLine, PreviousScreen, Next, NextLine, NextScreen, Last, } /// Internal struct for searching in a file. This is protected by an Arc so /// that it can be accessed from both the main screen thread and also the search /// thread. struct SearchInner { pattern: String, kind: SearchKind, regex: Regex, matches: RwLock>, matching_lines: RwLock, current_match: RwLock>, matching_line_count: AtomicUsize, search_line_count: AtomicUsize, finished: AtomicBool, } /// A search for a pattern within a file. pub(crate) struct Search { inner: Arc, } impl SearchInner { /// Create a new SearchInner for a search. fn new( file: &File, pattern: &str, kind: SearchKind, event_sender: EventSender, ) -> Result, Error> { let regex = Regex::new(pattern)?; let search = Arc::new(SearchInner { pattern: pattern.to_string(), kind, regex: regex.clone(), matches: RwLock::new(Vec::new()), matching_lines: RwLock::new(BitSet::new()), current_match: RwLock::new(None), matching_line_count: AtomicUsize::new(0), search_line_count: AtomicUsize::new(0), finished: AtomicBool::new(false), }); thread::Builder::new() .name(String::from("sp-search")) .spawn({ let search = search.clone(); let file = file.clone(); move || { let mut matched = false; loop { let loaded = file.loaded(); let lines = file.lines(); let search_line_count = search.search_line_count.load(Ordering::SeqCst); let search_limit = min( search_line_count + SEARCH_BATCH_SIZE, if loaded { lines } else { lines - 1 }, ); for line in search_line_count..search_limit { let count = file.with_line(line, |data| { // Strip trailing LF or CRLF if it is there. let len = trim_trailing_newline(&data[..]); let data = overstrike::convert_overstrike(&data[..len]); let data = ESCAPE_SEQUENCE.replace_all(&data[..], NoExpand(b"")); regex.find_iter(&data[..]).count() }); if count.unwrap_or(0) > 0 { let mut matching_lines = search.matching_lines.write().unwrap(); matching_lines.insert(line); let mut matches = search.matches.write().unwrap(); let first_match_index = matches.len(); for i in 0..count.unwrap() { matches.push((line, i)); } search.matching_line_count.fetch_add(1, Ordering::SeqCst); if !matched { if let Some(index) = match search.kind { SearchKind::First => Some(first_match_index), SearchKind::FirstAfter(offset) => { if line >= offset { Some(first_match_index) } else { None } } SearchKind::FirstBefore(offset) => { if line >= offset && first_match_index > 0 && matches[first_match_index - 1].0 < offset { Some(first_match_index - 1) } else { None } } } { *search.current_match.write().unwrap() = Some(index); event_sender .send(Event::SearchFirstMatch(file.index())) .unwrap(); matched = true; } } } } search .search_line_count .store(search_limit, Ordering::SeqCst); if loaded && search_limit == lines { // Searched the whole file. break; } if !loaded && search_limit >= lines - 1 { // Searched the whole file so far. Wait for more data. thread::sleep(time::Duration::from_millis(100)); } } if !matched { let matches = search.matches.read().unwrap(); if matches.len() > 0 { let index = match search.kind { SearchKind::First | SearchKind::FirstAfter(_) => 0, SearchKind::FirstBefore(_) => matches.len() - 1, }; *search.current_match.write().unwrap() = Some(index); event_sender .send(Event::SearchFirstMatch(file.index())) .unwrap(); } } search.finished.store(true, Ordering::SeqCst); event_sender .send(Event::SearchFinished(file.index())) .unwrap(); } }) .unwrap(); Ok(search) } } impl Search { /// Create a new search for a pattern. pub(crate) fn new( file: &File, pattern: &str, kind: SearchKind, event_sender: EventSender, ) -> Result { Ok(Search { inner: SearchInner::new(file, pattern, kind, event_sender)?, }) } /// Returns true if the search has finished searching the whole file. pub(crate) fn finished(&self) -> bool { self.inner.finished.load(Ordering::SeqCst) } /// Renders the search overlay line. pub(crate) fn render(&mut self, changes: &mut Vec, line: usize, width: usize) { let mut width = width; changes.push(Change::CursorPosition { x: Position::Absolute(0), y: Position::Absolute(line), }); changes.push(Change::AllAttributes( CellAttributes::default() .set_foreground(AnsiColor::Black) .set_background(AnsiColor::Silver) .clone(), )); if width < 8 { // The screen is too small to write anything, just write a blank bar. changes.push(Change::ClearToEndOfLine(AnsiColor::Silver.into())); return; } changes.push(Change::Text(" ".into())); width -= 2; let matches = self.inner.matches.read().unwrap(); let match_info = match *self.inner.current_match.read().unwrap() { Some(index) => Cow::Owned(format!( "{} of {} matches on {} lines", index + 1, matches.len(), self.inner.matching_line_count.load(Ordering::SeqCst), )), _ if self.inner.finished.load(Ordering::SeqCst) => Cow::Borrowed("No matches"), _ => Cow::Owned(format!( "Searched {} lines", self.inner.search_line_count.load(Ordering::SeqCst), )), }; // The right-hand side is shown only if it can fit. let right_width = match_info.width() + 2; let mut left_width = width; if width >= right_width { left_width -= right_width; } // Write the left-hand side if it fits. match left_width { 0 => {} 1 => changes.push(Change::Text(" ".into())), _ => changes.push(Change::Text(format!( "{1:0$.0$} ", left_width - 1, self.inner.pattern ))), } // Write the right-hand side if it fits. if width >= right_width { changes.push(Change::Text(match_info.into())); changes.push(Change::ClearToEndOfLine(AnsiColor::Silver.into())); } } /// Returns the line number and match index of the current match. pub(crate) fn current_match(&self) -> Option<(usize, usize)> { let matches = self.inner.matches.read().unwrap(); let current_match_index = self.inner.current_match.read().unwrap(); current_match_index.map(|index| matches[index]) } /// Moves to another match if there is one. /// /// `scope` describes visible lines of the file on screen. /// It is used for `*Screen` movements. pub(crate) fn move_match(&mut self, motion: MatchMotion, scope: RangeInclusive) { let matches = self.inner.matches.read().unwrap(); if matches.len() > 0 { let mut current_match_index = self.inner.current_match.write().unwrap(); if let Some(ref mut index) = *current_match_index { // If the current match is within `line_scope`, then `*Screen` is just `*` movement. let need_seek = matches!( motion, MatchMotion::NextScreen | MatchMotion::PreviousScreen ) && !scope.contains(&matches[*index].0); match motion { MatchMotion::First => *index = 0, MatchMotion::PreviousLine => { let match_index = matches[*index].1; if match_index < *index { *index -= match_index + 1; } } MatchMotion::Previous | MatchMotion::PreviousScreen if *index > 0 => { *index -= 1 } MatchMotion::Next | MatchMotion::NextScreen if *index < matches.len() - 1 => { *index += 1 } MatchMotion::NextLine => { let line_index = matches[*index].0; let mut new_index = *index; while new_index < matches.len() - 1 && matches[new_index].0 == line_index { new_index += 1; } if matches[new_index].0 != line_index { *index = new_index; } } MatchMotion::Last => *index = matches.len() - 1, _ => {} } // Attempt to satisfy the scope limit. if need_seek { match motion { MatchMotion::NextScreen => { let mut candidate_index = *index; if matches[candidate_index].0 > *scope.end() { // Re-search from the beginning. candidate_index = 0; } // Search forward. while candidate_index < matches.len() - 1 { if matches[candidate_index].0 >= *scope.start() { *index = candidate_index; break; } candidate_index += 1; } } MatchMotion::PreviousScreen => { let mut candidate_index = *index; if matches[candidate_index].0 < *scope.start() { // Re-search from the end. candidate_index = matches.len() - 1; } // Search backward. while candidate_index > 0 { if matches[candidate_index].0 <= *scope.end() { *index = candidate_index; break; } candidate_index -= 1; } } _ => {} } } } } } /// Returns the lines in the given range that match. pub(crate) fn matching_lines(&self, start: usize, end: usize) -> Vec { let mut lines = Vec::new(); let matching_lines = self.inner.matching_lines.read().unwrap(); for line in start..end { if matching_lines.contains(line) { lines.push(line); } } lines } /// Returns the number of searched lines. pub(crate) fn searched_lines(&self) -> usize { self.inner.search_line_count.load(Ordering::SeqCst) } /// Returns the Regex used for this search. pub(crate) fn regex(&self) -> &Regex { &self.inner.regex } /// Returns true if the line index matches the search pub(crate) fn line_matches(&self, line_index: usize) -> bool { self.inner .matching_lines .read() .unwrap() .contains(line_index) } } pub(crate) fn trim_trailing_newline(data: impl AsRef<[u8]>) -> usize { let data = data.as_ref(); let mut len = data.len(); if len > 0 && data[len - 1] == b'\n' { len -= 1; if len > 0 && data[len - 1] == b'\r' { len -= 1; } } len } sapling-streampager-0.11.0/src/util.rs000064400000000000000000000032411046102023000157620ustar 00000000000000//! Utilities. use std::borrow::Cow; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; /// Returns the maximum width in characters of a number. pub(crate) fn number_width(number: usize) -> usize { let mut width = 1; let mut limit = 10; while limit <= number { limit *= 10; width += 1; } width } /// Truncates a string to a column offset and width. pub(crate) fn truncate_string<'a>( text: impl Into>, offset: usize, width: usize, ) -> String { let text = text.into(); if offset > 0 || width < text.width() { let mut column = 0; let mut maybe_start_index = None; let mut maybe_end_index = None; let mut start_pad = 0; let mut end_pad = 0; for (i, g) in text.grapheme_indices(true) { let w = g.width(); if w != 0 { if column >= offset && maybe_start_index.is_none() { maybe_start_index = Some(i); start_pad = column - offset; } if column + w > offset + width && maybe_end_index.is_none() { maybe_end_index = Some(i); end_pad = offset + width - column; break; } column += w; } } let start_index = maybe_start_index.unwrap_or(text.len()); let end_index = maybe_end_index.unwrap_or(text.len()); format!( "{0:1$.1$}{3}{0:2$.2$}", "", start_pad, end_pad, &text[start_index..end_index] ) } else { text.into_owned() } }