diff --git a/Cargo.lock b/Cargo.lock index 51c7756..e4d74af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,389 +2,380 @@ # It is not intended for manual editing. [[package]] name = "aho-corasick" -version = "0.7.10" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" dependencies = [ - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", ] [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "hermit-abi", + "libc", + "winapi", ] [[package]] name = "autocfg" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "clap" -version = "3.0.0-beta.1" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" dependencies = [ - "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "clap_derive 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "os_str_bytes 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "strsim 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", - "term_size 1.0.0-beta1 (registry+https://github.com/rust-lang/crates.io-index)", - "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "terminal_size", + "textwrap", + "unicode-width", + "vec_map", ] [[package]] name = "clap_derive" -version = "3.0.0-beta.1" +version = "3.0.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" dependencies = [ - "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro-error 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "copyrat" version = "0.1.0" dependencies = [ - "clap 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "sequence_trie 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "duct", + "regex", + "sequence_trie", + "termion", ] [[package]] -name = "heck" -version = "0.3.1" +name = "duct" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc6a0a59ed0888e0041cf708e66357b7ae1a82f1c67247e1f93b5e0818f7d8d" dependencies = [ - "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" + +[[package]] +name = "heck" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" +dependencies = [ + "unicode-segmentation", ] [[package]] name = "hermit-abi" -version = "0.1.12" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" dependencies = [ - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] [[package]] name = "indexmap" -version = "1.3.2" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" dependencies = [ - "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "autocfg", + "hashbrown", ] [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.69" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b07a082330a35e43f63177cc01689da34fbffa0105e1246cf0311472cac73a" [[package]] name = "memchr" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "numtoa" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" + +[[package]] +name = "os_pipe" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "os_str_bytes" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" [[package]] name = "proc-macro-error" -version = "0.4.12" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ - "proc-macro-error-attr 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", ] [[package]] name = "proc-macro-error-attr" -version = "0.4.12" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", - "syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.14" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" dependencies = [ - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid", ] [[package]] name = "quote" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", ] [[package]] name = "redox_syscall" -version = "0.1.56" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] [[package]] name = "redox_termios" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" dependencies = [ - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall", ] [[package]] name = "regex" -version = "1.3.7" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54fd1046a3107eb58f42de31d656fee6853e5d276c455fd943742dce89fc3dd3" dependencies = [ - "aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.17" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" [[package]] name = "sequence_trie" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee22067b7ccd072eeb64454b9c6e1b33b61cd0d49e895fd48676a184580e0c3" + +[[package]] +name = "shared_child" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6be9f7d5565b1483af3e72975e2dee33879b3b86bd48c0929fccf6585d79e65a" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.23" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd9bc7ccc2688b3344c2f48b9b546648b25ce0b20fc717ee7fa7981a8ca9717" dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "syn-mid" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "term_size" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "term_size" -version = "1.0.0-beta1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "unicode-xid", ] [[package]] name = "termcolor" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" dependencies = [ - "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ca8ced750734db02076f44132d802af0b33b09942331f4459dde8636fd2406" +dependencies = [ + "libc", + "winapi", ] [[package]] name = "termion" -version = "1.5.5" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" dependencies = [ - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", - "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", - "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", + "numtoa", + "redox_syscall", + "redox_termios", ] [[package]] name = "textwrap" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" dependencies = [ - "term_size 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "thread_local" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "terminal_size", + "unicode-width", ] [[package]] name = "unicode-segmentation" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" [[package]] name = "unicode-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "vec_map" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" [[package]] name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[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.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" - -[metadata] -"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" -"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" -"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" -"checksum clap 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)" = "860643c53f980f0d38a5e25dfab6c3c93b2cb3aa1fe192643d17a293c6c41936" -"checksum clap_derive 3.0.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fb51c9e75b94452505acd21d929323f5a5c6c4735a852adbd39ef5fb1b014f30" -"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" -"checksum hermit-abi 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" -"checksum indexmap 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "076f042c5b7b98f31d205f1249267e12a6518c1481e9dae9764af19b707d2292" -"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -"checksum libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)" = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" -"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" -"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" -"checksum os_str_bytes 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "06de47b848347d8c4c94219ad8ecd35eb90231704b067e67e6ae2e36ee023510" -"checksum proc-macro-error 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" -"checksum proc-macro-error-attr 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" -"checksum proc-macro2 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)" = "de40dd4ff82d9c9bab6dae29dbab1167e515f8df9ed17d2987cb6012db206933" -"checksum quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "54a21852a652ad6f610c9510194f398ff6f8692e334fd1145fed931f7fbe44ea" -"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" -"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" -"checksum regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" -"checksum regex-syntax 0.6.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" -"checksum sequence_trie 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1ee22067b7ccd072eeb64454b9c6e1b33b61cd0d49e895fd48676a184580e0c3" -"checksum strsim 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -"checksum syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "95b5f192649e48a5302a13f2feb224df883b98933222369e4b3b0fe2a5447269" -"checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" -"checksum term_size 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" -"checksum term_size 1.0.0-beta1 (registry+https://github.com/rust-lang/crates.io-index)" = "a8a17d8699e154863becdf18e4fd28bd0be27ca72856f54daf75c00f2566898f" -"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" -"checksum termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905" -"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" -"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" -"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" -"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" -"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" -"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" -"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" -"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index ab11bcb..acfa05d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,24 @@ [package] name = "copyrat" version = "0.1.0" -authors = ["Ferran Basora ", "u0xy "] +authors = ["u0xy "] edition = "2018" description = "This is tmux-copycat on Rust steroids." -repository = "https://github.com/fcsonline/tmux-thumbs" +repository = "https://github.com/graelo/tmux-copyrat" keywords = ["rust", "tmux", "tmux-plugin", "tmux-copycat"] license = "MIT" [dependencies] termion = "1.5" -regex = "1.3.1" -clap = { version = "3.0.0-beta.1", features = ["suggestions", "color", "wrap_help", "term_size"]} +regex = "1.4" +clap = { version = "3.0.0-beta.2", features = ["suggestions", "color", "wrap_help"]} sequence_trie = "0.3.6" +duct = "0.13" [[bin]] name = "copyrat" -path = "src/main.rs" +path = "src/bin/copyrat.rs" [[bin]] name = "tmux-copyrat" -path = "src/bridge.rs" +path = "src/bin/tmux_copyrat.rs" diff --git a/README.md b/README.md index e8f3d6a..2407a25 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ cargo build --release Source it in your `.tmux.conf`: ``` -run-shell ~/.tmux/plugins/tmux-copyrat/tmux-copyrat.tmux +run-shell ~/.tmux/plugins/tmux-copyrat/copyrat.tmux ``` Reload TMUX conf by running: diff --git a/copyrat.tmux b/copyrat.tmux new file mode 100755 index 0000000..3097848 --- /dev/null +++ b/copyrat.tmux @@ -0,0 +1,114 @@ +#!/usr/bin/env zsh + +# This scripts provides a default configuration for tmux-copyrat options and key bindings. +# It is run only once at tmux launch. +# +# Each option and binding can be overridden in your `tmux.conf` by defining options like +# +# set -g @copyrat-keytable "foobar" +# set -g @copyrat-keyswitch "z" +# set -g @copyrat-match-bg "magenta" +# +# and bindings like +# +# bind-key -T foobar h new-window -d -n "[copyrat]" '/path/to/tmux-copyrat --window-name "[copyrat]" --pattern-name urls' +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# changing this script may break integration with `tmux-copyrat`. +# + +# Just make sure you first open a named window in the background and provide +# that name to the binary `tmux-copyrat`. +# +# Don't even try to run tmux-copyrat with run-shell, this cannot work because +# Tmux launches these processes without attaching them to a pty. + +# You can also entirely ignore this file (not even source it) and define all +# options and bindings in your `tmux.conf`. + +CURRENT_DIR="$( cd "$( dirname "$0" )" && pwd )" +BINARY="${CURRENT_DIR}/tmux-copyrat" + + +# +# Top-level options +# + +setup_option() { + local opt_name=$1 + local default_value=$2 + local current_value=$(tmux show-option -gqv @copyrat-${opt_name}) + value=${current_value:-${default_value}} + tmux set-option -g @copyrat-${opt_name} ${value} +} + + +# Sets the window name which copyrat should use when running, providing a +# default value in case @copyrat-window-name was not defined. +setup_option "window-name" "[copyrat]" + +# Get that window name as a local variable for use in pattern bindings below. +window_name=$(tmux show-option -gqv @copyrat-window-name) + +# Sets the keytable for all bindings, providing a default if @copyrat-keytable was not defined. +# Keytables open a new shortcut space: if 't' is the switcher (see below), prefix + t + +setup_option "keytable" "cpyrt" + +# Sets the key to access the keytable: prefix + + +# providing a default if @copyrat-keyswitch is not defined. +setup_option "keyswitch" "t" + +keyswitch=$(tmux show-option -gv @copyrat-keyswitch) +keytable=$(tmux show-option -gv @copyrat-keytable) +tmux bind-key ${keyswitch} switch-client -T ${keytable} + + +# +# Pattern bindings +# + +setup_pattern_binding() { + local key=$1 + local pattern_arg="$2" + # The default window name `[copyrat]` has to be single quoted because it is + # interpreted by the shell when launched by tmux. + tmux bind-key -T ${keytable} ${key} new-window -d -n ${window_name} "${BINARY} --window-name '"${window_name}"' --reverse --unique-hint ${pattern_arg}" +} + +# prefix + t + p searches for absolute & relative paths +setup_pattern_binding "p" "--pattern-name path" +# prefix + t + u searches for URLs +setup_pattern_binding "u" "--pattern-name url" +# prefix + t + m searches for Markdown URLs [...](matched.url) +setup_pattern_binding "m" "--pattern-name markdown-url" +# prefix + t + h searches for SHA1/2 (hashes) +setup_pattern_binding "h" "--pattern-name sha" +# prefix + t + e searches for email addresses (see https://www.regular-expressions.info/email.html) +setup_pattern_binding "e" "--pattern-name email" +# prefix + t + D searches for docker shas +setup_pattern_binding "D" "--pattern-name docker" +# prefix + t + c searches for hex colors #aa00f5 +setup_pattern_binding "c" "--pattern-name hexcolor" +# prefix + t + U searches for UUIDs +setup_pattern_binding "U" "--pattern-name uuid" +# prefix + t + v searches for version numbers +setup_pattern_binding "v" "--pattern-name version" +# prefix + t + d searches for any string of 4+ digits +setup_pattern_binding "d" "--pattern-name digits" +# prefix + t + m searches for hex numbers: 0xbedead +setup_pattern_binding "m" "--pattern-name mem-address" +# prefix + t + 4 searches for IPV4 +setup_pattern_binding "4" "--pattern-name ipv4" +# prefix + t + 6 searches for IPV6 +setup_pattern_binding "6" "--pattern-name ipv6" +# prefix + t + Space searches for all known patterns (noisy and potentially slower) +setup_pattern_binding "space" "--all-patterns" + +# prefix + t + / prompts for a pattern and search for it +tmux bind-key -T ${keytable} "/" command-prompt -p "search:" "new-window -d -n '${window_name}' \"${BINARY}\" --window-name '${window_name}' --reverse --unique-hint --custom-pattern %%" + + +# Auto-install is currently disabled as it requires the user to have cargo installed. +# if [ ! -f "$BINARY" ]; then +# cd "${CURRENT_DIR}" && cargo build --release +# fi diff --git a/src/bin/copyrat.rs b/src/bin/copyrat.rs new file mode 100644 index 0000000..b665441 --- /dev/null +++ b/src/bin/copyrat.rs @@ -0,0 +1,28 @@ +use clap::Clap; +use std::io::{self, Read}; + +use copyrat::{config::basic, run, ui::Selection}; + +fn main() { + let opt = basic::Config::parse(); + + // Copy the pane contents (piped in via stdin) into a buffer, and split lines. + let stdin = io::stdin(); + let mut handle = stdin.lock(); + + let mut buffer = String::new(); + handle.read_to_string(&mut buffer).unwrap(); + let lines = buffer.split('\n').collect::>(); + + // Execute copyrat over the buffer (will take control over stdout). + // This returns the selected matche. + let selection: Option = run(&lines, &opt); + + // Early exit, signaling no selections were found. + if selection.is_none() { + std::process::exit(1); + } + + let Selection { text, .. } = selection.unwrap(); + println!("{}", text); +} diff --git a/src/bin/tmux_copyrat.rs b/src/bin/tmux_copyrat.rs new file mode 100644 index 0000000..bba6f79 --- /dev/null +++ b/src/bin/tmux_copyrat.rs @@ -0,0 +1,60 @@ +use copyrat::{ + config::extended::{ConfigExt, OutputDestination}, + error, tmux, + ui::Selection, +}; + +/// +fn main() -> Result<(), error::ParseError> { + let config = ConfigExt::initialize()?; + + // Identify active pane and capture its content. + let panes: Vec = tmux::list_panes()?; + + let active_pane = panes + .into_iter() + .find(|p| p.is_active) + .expect("Exactly one tmux pane should be active in the current window."); + + let buffer = tmux::capture_pane(&active_pane, &config.capture_region)?; + let lines = buffer.split('\n').collect::>(); + + // We have to dance a little with Panes, because this process' i/o streams + // are connected to the pane in the window newly created for us, instead + // of the active current pane. + let temp_pane_spec = format!("{}.0", config.window_name); + tmux::swap_pane_with(&temp_pane_spec)?; + + let selection = copyrat::run(&lines, &config.basic_config); + + tmux::swap_pane_with(&temp_pane_spec)?; + + // Finally copy selection to the output destination (tmux buffer or + // clipboard), and paste it to the active buffer if it was uppercased. + + match selection { + None => return Ok(()), + Some(Selection { + text, + uppercased, + output_destination, + }) => { + if uppercased { + duct::cmd!("tmux", "send-keys", "-t", active_pane.id.as_str(), &text).run()?; + } + + match output_destination { + OutputDestination::Tmux => { + duct::cmd!("tmux", "set-buffer", &text).run()?; + } + OutputDestination::Clipboard => { + duct::cmd!("echo", "-n", &text) + .pipe(duct::cmd!(config.clipboard_exe)) + .read()?; + } + } + } + } + + Ok(()) +} diff --git a/src/bridge.rs b/src/bridge.rs deleted file mode 100644 index 45e4304..0000000 --- a/src/bridge.rs +++ /dev/null @@ -1,135 +0,0 @@ -use clap::Clap; -use std::collections::HashMap; -use std::str::FromStr; - -use copyrat::{error, process, CliOpt}; - -mod tmux; - -/// Main configuration, parsed from command line. -#[derive(Clap, Debug)] -#[clap(author, about, version)] -struct BridgeOpt { - /// Don't read options from Tmux. - /// - /// By default, options formatted like `copyrat-*` are read from tmux. - /// However, you should consider reading them from the config file (the - /// default option) as this saves both a command call (about 10ms) and a - /// Regex compilation. - #[clap(long)] - ignore_options_from_tmux: bool, - - /// Name of the copyrat temporary window. - /// - /// Copyrat is launched in a temporary window of that name. The only pane - /// in this temp window gets swapped with the current active one for - /// in-place searching, then swapped back and killed after we exit. - #[clap(long, default_value = "[copyrat]")] - window_name: String, - - /// Capture visible area or entire pane history. - #[clap(long, arg_enum, default_value = "visible-area")] - capture_region: tmux::CaptureRegion, - - // Include CLI Options - #[clap(flatten)] - cli_options: CliOpt, -} - -impl BridgeOpt { - /// Try parsing provided options, and update self with the valid values. - /// Unknown options are simply ignored. - pub fn merge_map( - &mut self, - options: &HashMap, - ) -> Result<(), error::ParseError> { - for (name, value) in options { - match name.as_ref() { - "@copyrat-capture" => { - self.capture_region = tmux::CaptureRegion::from_str(&value)?; - } - _ => (), - } - } - - // Pass the call to cli_options. - self.cli_options.merge_map(options)?; - - Ok(()) - } -} - -/// -fn main() -> Result<(), error::ParseError> { - let mut opt = BridgeOpt::parse(); - - if !opt.ignore_options_from_tmux { - let tmux_options: HashMap = tmux::get_options("@copyrat-")?; - - // Override default values with those coming from tmux. - opt.merge_map(&tmux_options)?; - } - - // Identify active pane and capture its content. - let panes: Vec = tmux::list_panes()?; - - let active_pane = panes - .into_iter() - .find(|p| p.is_active) - .expect("Exactly one tmux pane should be active in the current window."); - - let buffer = tmux::capture_pane(&active_pane, &opt.capture_region)?; - - // We have to dance a little with Panes, because this process i/o streams - // are connected to the pane in the window newly created for us, instead - // of the active current pane. - let temp_pane_spec = format!("{}.0", opt.window_name); - tmux::swap_pane_with(&temp_pane_spec)?; - - let selections = copyrat::run(buffer, &opt.cli_options); - - tmux::swap_pane_with(&temp_pane_spec)?; - - // Finally copy selection to a tmux buffer, and paste it to the active - // buffer if it was uppercased. - // TODO: consider getting rid of multi-selection mode. - - // Execute a command on each group of selections (normal and uppercased). - let (normal_selections, uppercased_selections): (Vec<(String, bool)>, Vec<(String, bool)>) = - selections - .into_iter() - .partition(|(_text, uppercased)| !*uppercased); - - let buffer_selections: String = normal_selections - .into_iter() - .map(|(text, _)| text) - .collect::>() - .join("\n"); - - if buffer_selections.len() > 0 { - let args = vec!["set-buffer", &buffer_selections]; - // Simply execute the command as is, and let the program crash on - // potential errors because it is not our responsibility. - process::execute("tmux", &args).unwrap(); - } - - let buffer_selections: String = uppercased_selections - .into_iter() - .map(|(text, _)| text) - .collect::>() - .join("\n"); - - if buffer_selections.len() > 0 { - let args = vec!["set-buffer", &buffer_selections]; - // Simply execute the command as is, and let the program crash on - // potential errors because it is not our responsibility. - process::execute("tmux", &args).unwrap(); - - let args = vec!["paste-buffer", "-t", active_pane.id.as_str()]; - // Simply execute the command as is, and let the program crash on - // potential errors because it is not our responsibility. - process::execute("tmux", &args).unwrap(); - } - - Ok(()) -} diff --git a/src/colors.rs b/src/colors.rs deleted file mode 100644 index c3614db..0000000 --- a/src/colors.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::error; -use termion::color; - -pub fn parse_color(src: &str) -> Result, error::ParseError> { - match src { - "black" => Ok(Box::new(color::Black)), - "red" => Ok(Box::new(color::Red)), - "green" => Ok(Box::new(color::Green)), - "yellow" => Ok(Box::new(color::Yellow)), - "blue" => Ok(Box::new(color::Blue)), - "magenta" => Ok(Box::new(color::Magenta)), - "cyan" => Ok(Box::new(color::Cyan)), - "white" => Ok(Box::new(color::White)), - "bright-black" => Ok(Box::new(color::LightBlack)), - "bright-red" => Ok(Box::new(color::LightRed)), - "bright-green" => Ok(Box::new(color::LightGreen)), - "bright-yellow" => Ok(Box::new(color::LightYellow)), - "bright-blue" => Ok(Box::new(color::LightBlue)), - "bright-magenta" => Ok(Box::new(color::LightMagenta)), - "bright-cyan" => Ok(Box::new(color::LightCyan)), - "bright-white" => Ok(Box::new(color::LightWhite)), - // "default" => Ok(Box::new(color::Reset)), - _ => Err(error::ParseError::UnknownColor), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn match_color() { - let text1 = format!( - "{}{}", - color::Fg(parse_color("green").unwrap().as_ref()), - "foo" - ); - let text2 = format!("{}{}", color::Fg(color::Green), "foo"); - - assert_eq!(text1, text2); - } - - #[test] - fn no_match_color() { - assert!(parse_color("wat").is_err(), "this color should not exist"); - } -} diff --git a/src/config/basic.rs b/src/config/basic.rs new file mode 100644 index 0000000..3859d6e --- /dev/null +++ b/src/config/basic.rs @@ -0,0 +1,103 @@ +use clap::Clap; +use std::str::FromStr; + +use crate::{ + error, + textbuf::{alphabet, regexes}, + ui, +}; + +/// Main configuration, parsed from command line. +#[derive(Clap, Debug)] +#[clap(author, about, version)] +pub struct Config { + /// Alphabet to draw hints from. + /// + /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", + /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz" + /// "dvorak", "colemak". + /// + /// # Examples + /// + /// "qwerty", "dvorak-homerow", "azerty-right-hand". + #[clap(short = 'k', long, default_value = "dvorak", + parse(try_from_str = alphabet::parse_alphabet))] + pub alphabet: alphabet::Alphabet, + + /// Use all available regex patterns. + #[clap(short = 'A', long = "--all-patterns")] + pub use_all_patterns: bool, + + /// Pattern names to use ("email", ... see doc). + #[clap(short = 'x', long = "--pattern-name", parse(try_from_str = regexes::parse_pattern_name))] + pub named_patterns: Vec, + + /// Additional regex patterns ("foo*bar", etc). + #[clap(short = 'X', long = "--custom-pattern")] + pub custom_patterns: Vec, + + /// Assign hints starting from the bottom of the screen. + #[clap(short, long)] + pub reverse: bool, + + /// Keep the same hint for identical matches. + #[clap(short, long)] + pub unique_hint: bool, + + /// Move focus back to first/last match. + #[clap(short = 'w', long)] + pub focus_wrap_around: bool, + + #[clap(flatten)] + pub colors: ui::colors::UiColors, + + /// Align hint with its match. + #[clap(long, arg_enum, default_value = "leading")] + pub hint_alignment: ui::HintAlignment, + + /// Optional hint styling. + /// + /// Underline or surround the hint for increased visibility. + /// If not provided, only the hint colors will be used. + #[clap(short = 's', long, arg_enum)] + pub hint_style: Option, + + /// Chars surrounding each hint, used with `Surround` style. + #[clap(long, default_value = "{}", + parse(try_from_str = parse_chars))] + pub hint_surroundings: (char, char), +} + +/// Type introduced due to parsing limitation, +/// as we cannot directly parse into ui::HintStyle. +#[derive(Debug, Clap)] +pub enum HintStyleArg { + Bold, + Italic, + Underline, + Surround, +} + +impl FromStr for HintStyleArg { + type Err = error::ParseError; + + fn from_str(s: &str) -> Result { + match s { + "leading" => Ok(HintStyleArg::Underline), + "trailing" => Ok(HintStyleArg::Surround), + _ => Err(error::ParseError::ExpectedString(String::from( + "underline or surround", + ))), + } + } +} + +/// Try to parse a `&str` into a tuple of `char`s. +fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { + if src.chars().count() != 2 { + return Err(error::ParseError::ExpectedSurroundingPair); + } + + let chars: Vec = src.chars().collect(); + Ok((chars[0], chars[1])) +} diff --git a/src/config/extended.rs b/src/config/extended.rs new file mode 100644 index 0000000..52f077a --- /dev/null +++ b/src/config/extended.rs @@ -0,0 +1,170 @@ +use clap::Clap; +use std::collections::HashMap; +use std::fmt; +use std::str::FromStr; + +use super::basic; +use crate::{ + error, + textbuf::{alphabet, regexes}, + tmux, ui, +}; + +/// Extended configuration for handling Tmux-specific configuration (options +/// and outputs). This is only used by `tmux-copyrat` and parsed from command +/// line.. +#[derive(Clap, Debug)] +#[clap(author, about, version)] +pub struct ConfigExt { + /// Don't read options from Tmux. + /// + /// By default, options formatted like `copyrat-*` are read from tmux. + /// However, you should consider reading them from the config file (the + /// default option) as this saves both a command call (about 10ms) and a + /// Regex compilation. + #[clap(short = 'n', long)] + pub ignore_tmux_options: bool, + + /// Name of the copyrat temporary Tmux window. + /// + /// Copyrat is launched in a temporary window of that name. The only pane + /// in this temp window gets swapped with the current active one for + /// in-place searching, then swapped back and killed after we exit. + #[clap(short = 'W', long, default_value = "[copyrat]")] + pub window_name: String, + + /// Capture visible area or entire pane history. + #[clap(long, arg_enum, default_value = "visible-area")] + pub capture_region: CaptureRegion, + + /// Name of the copy-to-clipboard executable. + /// + /// If during execution, the output destination is set to be clipboard, + /// then copyrat will pipe the selected text to this executable. + #[clap(long, default_value = "pbcopy")] + pub clipboard_exe: String, + + // Include fields from the basic config + #[clap(flatten)] + pub basic_config: basic::Config, +} + +impl ConfigExt { + pub fn initialize() -> Result { + let mut config_ext = ConfigExt::parse(); + + if !config_ext.ignore_tmux_options { + let tmux_options: HashMap = tmux::get_options("@copyrat-")?; + + // Override default values with those coming from tmux. + let wrapped = &mut config_ext.basic_config; + + for (name, value) in &tmux_options { + match name.as_ref() { + "@copyrat-capture" => { + config_ext.capture_region = CaptureRegion::from_str(&value)? + } + "@copyrat-alphabet" => { + wrapped.alphabet = alphabet::parse_alphabet(value)?; + } + "@copyrat-pattern-name" => { + wrapped.named_patterns = vec![regexes::parse_pattern_name(value)?] + } + "@copyrat-custom-pattern" => { + wrapped.custom_patterns = vec![String::from(value)] + } + "@copyrat-reverse" => { + wrapped.reverse = value.parse::()?; + } + "@copyrat-unique-hint" => { + wrapped.unique_hint = value.parse::()?; + } + + "@copyrat-match-fg" => { + wrapped.colors.match_fg = ui::colors::parse_color(value)? + } + "@copyrat-match-bg" => { + wrapped.colors.match_bg = ui::colors::parse_color(value)? + } + "@copyrat-focused-fg" => { + wrapped.colors.focused_fg = ui::colors::parse_color(value)? + } + "@copyrat-focused-bg" => { + wrapped.colors.focused_bg = ui::colors::parse_color(value)? + } + "@copyrat-hint-fg" => wrapped.colors.hint_fg = ui::colors::parse_color(value)?, + "@copyrat-hint-bg" => wrapped.colors.hint_bg = ui::colors::parse_color(value)?, + + "@copyrat-hint-alignment" => { + wrapped.hint_alignment = ui::HintAlignment::from_str(&value)? + } + "@copyrat-hint-style" => { + wrapped.hint_style = Some(basic::HintStyleArg::from_str(&value)?) + } + + // Ignore unknown options. + _ => (), + } + } + } + + Ok(config_ext) + } +} + +#[derive(Clap, Debug)] +pub enum CaptureRegion { + /// The entire history. + /// + /// This will end up sending `-S - -E -` to `tmux capture-pane`. + EntireHistory, + /// The visible area. + VisibleArea, + ///// Region from start line to end line + ///// + ///// This works as defined in tmux's docs (order does not matter). + //Region(i32, i32), +} + +impl FromStr for CaptureRegion { + type Err = error::ParseError; + + fn from_str(s: &str) -> Result { + match s { + "leading" => Ok(CaptureRegion::EntireHistory), + "trailing" => Ok(CaptureRegion::VisibleArea), + _ => Err(error::ParseError::ExpectedString(String::from( + "entire-history or visible-area", + ))), + } + } +} + +/// Describes the type of buffer the selected should be copied to: either a +/// tmux buffer or the system clipboard. +#[derive(Clone)] +pub enum OutputDestination { + /// The selection will be copied to the tmux buffer. + Tmux, + /// The selection will be copied to the system clipboard. + Clipboard, +} + +impl OutputDestination { + /// Toggle between the variants of `OutputDestination`. + pub fn toggle(&mut self) { + match *self { + Self::Tmux => *self = Self::Clipboard, + Self::Clipboard => *self = Self::Tmux, + } + } +} + +impl fmt::Display for OutputDestination { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Tmux => write!(f, "tmux buffer"), + Self::Clipboard => write!(f, "clipboard"), + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..26919fd --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,2 @@ +pub mod basic; +pub mod extended; diff --git a/src/lib.rs b/src/lib.rs index 742ba30..f61e584 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,7 @@ -use clap::Clap; -use std::collections::HashMap; -use std::path; -use std::str::FromStr; - -pub mod alphabets; -pub mod colors; +pub mod config; pub mod error; -pub mod model; -pub mod process; -pub mod regexes; +pub mod textbuf; +pub mod tmux; pub mod ui; /// Run copyrat on an input string `buffer`, configured by `Opt`. @@ -16,33 +9,41 @@ pub mod ui; /// # Note /// /// Maybe the decision to take ownership of the buffer is a bit bold. -pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { - let mut model = model::Model::new( - &buffer, +pub fn run(lines: &[&str], opt: &config::basic::Config) -> Option { + let model = textbuf::Model::new( + &lines, &opt.alphabet, - &opt.named_pattern, - &opt.custom_regex, + opt.use_all_patterns, + &opt.named_patterns, + &opt.custom_patterns, opt.reverse, + opt.unique_hint, ); + if model.matches.is_empty() { + return None; + } + let hint_style = match &opt.hint_style { None => None, Some(style) => match style { - HintStyleCli::Bold => Some(ui::HintStyle::Bold), - HintStyleCli::Italic => Some(ui::HintStyle::Italic), - HintStyleCli::Underline => Some(ui::HintStyle::Underline), - HintStyleCli::Surround => { + config::basic::HintStyleArg::Bold => Some(ui::HintStyle::Bold), + config::basic::HintStyleArg::Italic => Some(ui::HintStyle::Italic), + config::basic::HintStyleArg::Underline => Some(ui::HintStyle::Underline), + config::basic::HintStyleArg::Surround => { let (open, close) = opt.hint_surroundings; Some(ui::HintStyle::Surround(open, close)) } }, }; - let selection: Option<(String, bool)> = { - let mut ui = ui::Ui::new( - &mut model, - opt.unique_hint, + let default_output_destination = config::extended::OutputDestination::Tmux; + + let selection: Option = { + let mut ui = ui::ViewController::new( + &model, opt.focus_wrap_around, + default_output_destination, &opt.colors, &opt.hint_alignment, hint_style, @@ -53,145 +54,3 @@ pub fn run(buffer: String, opt: &CliOpt) -> Option<(String, bool)> { selection } - -/// Main configuration, parsed from command line. -#[derive(Clap, Debug)] -#[clap(author, about, version)] -pub struct CliOpt { - /// Alphabet to draw hints from. - /// - /// Possible values are "{A}", "{A}-homerow", "{A}-left-hand", - /// "{A}-right-hand", where "{A}" is one of "qwerty", "azerty", "qwertz" - /// "dvorak", "colemak". - /// - /// # Examples - /// - /// "qwerty", "dvorak-homerow", "azerty-right-hand". - #[clap(short = "k", long, default_value = "dvorak", - parse(try_from_str = alphabets::parse_alphabet))] - alphabet: alphabets::Alphabet, - - /// Pattern names to use (all if not specified). - #[clap(short = "x", long = "--pattern-name", parse(try_from_str = regexes::parse_pattern_name))] - named_pattern: Vec, - - /// Additional regex patterns. - #[clap(short = "X", long)] - custom_regex: Vec, - - /// Assign hints starting from the bottom of the screen. - #[clap(short, long)] - reverse: bool, - - /// Keep the same hint for identical matches. - #[clap(short, long)] - unique_hint: bool, - - #[clap(flatten)] - colors: ui::UiColors, - - /// Align hint with its match. - #[clap(short = "a", long, arg_enum, default_value = "leading")] - hint_alignment: ui::HintAlignment, - - /// Move focus back to first/last match. - #[clap(long)] - focus_wrap_around: bool, - - /// Optional hint styling. - /// - /// Underline or surround the hint for increased visibility. - /// If not provided, only the hint colors will be used. - #[clap(short = "s", long, arg_enum)] - hint_style: Option, - - /// Chars surrounding each hint, used with `Surround` style. - #[clap(long, default_value = "{}", - parse(try_from_str = parse_chars))] - hint_surroundings: (char, char), - - /// Optional target path where to store the selected matches. - #[clap(short = "o", long = "output", parse(from_os_str))] - pub target_path: Option, - - /// Describes if the uppercased marker should be added to the output, - /// indicating if hint key was uppercased. This is only used by - /// tmux-copyrat, so it is hidden (skipped) from the CLI. - #[clap(skip)] - pub uppercased_marker: bool, -} - -/// Type introduced due to parsing limitation, -/// as we cannot directly parse into ui::HintStyle. -#[derive(Debug, Clap)] -enum HintStyleCli { - Bold, - Italic, - Underline, - Surround, -} - -impl FromStr for HintStyleCli { - type Err = error::ParseError; - - fn from_str(s: &str) -> Result { - match s { - "leading" => Ok(HintStyleCli::Underline), - "trailing" => Ok(HintStyleCli::Surround), - _ => Err(error::ParseError::ExpectedString(String::from( - "underline or surround", - ))), - } - } -} - -/// Try to parse a `&str` into a tuple of `char`s. -fn parse_chars(src: &str) -> Result<(char, char), error::ParseError> { - if src.len() != 2 { - return Err(error::ParseError::ExpectedSurroundingPair); - } - - let chars: Vec = src.chars().collect(); - Ok((chars[0], chars[1])) -} - -impl CliOpt { - /// Try parsing provided options, and update self with the valid values. - pub fn merge_map( - &mut self, - options: &HashMap, - ) -> Result<(), error::ParseError> { - for (name, value) in options { - match name.as_ref() { - "@copyrat-alphabet" => { - self.alphabet = alphabets::parse_alphabet(value)?; - } - "@copyrat-regex-id" => (), // TODO - "@copyrat-custom-regex" => self.custom_regex = vec![String::from(value)], - "@copyrat-reverse" => { - self.reverse = value.parse::()?; - } - "@copyrat-unique-hint" => { - self.unique_hint = value.parse::()?; - } - - "@copyrat-match-fg" => self.colors.match_fg = colors::parse_color(value)?, - "@copyrat-match-bg" => self.colors.match_bg = colors::parse_color(value)?, - "@copyrat-focused-fg" => self.colors.focused_fg = colors::parse_color(value)?, - "@copyrat-focused-bg" => self.colors.focused_bg = colors::parse_color(value)?, - "@copyrat-hint-fg" => self.colors.hint_fg = colors::parse_color(value)?, - "@copyrat-hint-bg" => self.colors.hint_bg = colors::parse_color(value)?, - - "@copyrat-hint-alignment" => { - self.hint_alignment = ui::HintAlignment::from_str(&value)? - } - "@copyrat-hint-style" => self.hint_style = Some(HintStyleCli::from_str(&value)?), - - // Ignore unknown options. - _ => (), - } - } - - Ok(()) - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 2cee8da..0000000 --- a/src/main.rs +++ /dev/null @@ -1,43 +0,0 @@ -use clap::Clap; -use std::fs::OpenOptions; -use std::io::prelude::*; -use std::io::{self, Read}; - -use copyrat::{run, CliOpt}; - -fn main() { - let opt = CliOpt::parse(); - - // Copy the pane contents (piped in via stdin) into a buffer, and split lines. - let stdin = io::stdin(); - let mut handle = stdin.lock(); - - let mut buffer = String::new(); - handle.read_to_string(&mut buffer).unwrap(); - - // Execute copyrat over the buffer (will take control over stdout). - // This returns the selected matches. - let selection: Option<(String, bool)> = run(buffer, &opt); - - // Early exit, signaling no selections were found. - if selection.is_none() { - std::process::exit(1); - } - - let (text, _) = selection.unwrap(); - - // Write output to a target_path if provided, else print to original stdout. - match opt.target_path { - None => println!("{}", text), - Some(target) => { - let mut file = OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(target) - .expect("Unable to open the target file"); - - file.write(text.as_bytes()).unwrap(); - } - } -} diff --git a/src/model.rs b/src/model.rs deleted file mode 100644 index 4334fdc..0000000 --- a/src/model.rs +++ /dev/null @@ -1,559 +0,0 @@ -use std::collections; - -use regex::Regex; -use sequence_trie::SequenceTrie; - -use crate::alphabets::Alphabet; -use crate::regexes::{NamedPattern, EXCLUDE_PATTERNS, PATTERNS}; - -/// Holds data for the `Ui`. -pub struct Model<'a> { - // buffer: &'a str, - pub lines: Vec<&'a str>, - alphabet: &'a Alphabet, - named_patterns: &'a Vec, - custom_regexes: &'a Vec, - pub reverse: bool, -} - -impl<'a> Model<'a> { - pub fn new( - buffer: &'a str, - alphabet: &'a Alphabet, - named_patterns: &'a Vec, - custom_regexes: &'a Vec, - reverse: bool, - ) -> Model<'a> { - let lines = buffer.split('\n').collect(); - - Model { - // buffer, - lines, - alphabet, - named_patterns, - custom_regexes, - reverse, - } - } - - /// Returns a vector of `Match`es, each corresponding to a pattern match - /// in the lines, its location (x, y), and associated hint. - pub fn matches(&self, unique: bool) -> Vec> { - let mut raw_matches = self.raw_matches(); - - if self.reverse { - raw_matches.reverse(); - } - - let mut matches = self.associate_hints(&raw_matches, unique); - - if self.reverse { - matches.reverse(); - } - - matches - } - - /// Internal function that searches the model's lines for pattern matches. - /// Returns a vector of `RawMatch`es (text, location, pattern id) without - /// an associated hint. The hint is attached to `Match`, not to `RawMatch`. - /// - /// # Notes - /// - /// Custom regexes have priority over other regexes. - /// - /// If no named patterns were specified, it will search for all available - /// patterns from the `PATTERNS` catalog. - fn raw_matches(&self) -> Vec> { - let mut matches = Vec::new(); - - let exclude_regexes = EXCLUDE_PATTERNS - .iter() - .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) - .collect::>(); - - let custom_regexes = self - .custom_regexes - .iter() - .map(|pattern| { - ( - "custom", - Regex::new(pattern).expect("Invalid custom regexp"), - ) - }) - .collect::>(); - - let regexes = if self.named_patterns.is_empty() { - PATTERNS - .iter() - .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) - .collect::>() - } else { - self.named_patterns - .iter() - .map(|NamedPattern(name, pattern)| (name.as_str(), Regex::new(pattern).unwrap())) - .collect::>() - }; - - let all_regexes = [exclude_regexes, custom_regexes, regexes].concat(); - - for (index, line) in self.lines.iter().enumerate() { - // Remainder of the line to be searched for matches. - // This advances iteratively, until no matches can be found. - let mut chunk: &str = line; - let mut offset: i32 = 0; - - // Use all avail regexes to match the chunk and select the match - // occuring the earliest on the chunk. Save its matched text and - // position in a `Match` struct. - loop { - let chunk_matches = all_regexes - .iter() - .filter_map(|(&ref name, regex)| match regex.find_iter(chunk).nth(0) { - Some(m) => Some((name, regex, m)), - None => None, - }) - .collect::>(); - - if chunk_matches.is_empty() { - break; - } - - // First match on the chunk. - let (name, pattern, matching) = chunk_matches - .iter() - .min_by(|x, y| x.2.start().cmp(&y.2.start())) - .unwrap(); - - let text = matching.as_str(); - - let captures = pattern - .captures(text) - .expect("At this stage the regex must have matched."); - - // Handle both capturing and non-capturing patterns. - let (subtext, substart) = if let Some(capture) = captures.get(1) { - (capture.as_str(), capture.start()) - } else { - (text, 0) - }; - - // Never hint or break ansi color sequences. - if *name != "ansi_colors" { - matches.push(RawMatch { - x: offset + matching.start() as i32 + substart as i32, - y: index as i32, - pattern: name, - text: subtext, - }); - } - - chunk = chunk.get(matching.end()..).expect("Unknown chunk"); - offset += matching.end() as i32; - } - } - - matches - } - - /// Associate a hint to each `RawMatch`, returning a vector of `Match`es. - /// - /// If `unique` is `true`, all duplicate matches will have the same hint. - /// For copying matched text, this seems easier and more natural. - /// If `unique` is `false`, duplicate matches will have their own hint. - fn associate_hints(&self, raw_matches: &Vec>, unique: bool) -> Vec> { - let hints = self.alphabet.make_hints(raw_matches.len()); - let mut hints_iter = hints.iter(); - - let mut result: Vec> = vec![]; - - if unique { - // Map (text, hint) - let mut known: collections::HashMap<&str, &str> = collections::HashMap::new(); - - for raw_mat in raw_matches { - let hint: &str = known.entry(raw_mat.text).or_insert( - hints_iter - .next() - .expect("We should have as many hints as necessary, even invisible ones."), - ); - - result.push(Match { - x: raw_mat.x, - y: raw_mat.y, - pattern: raw_mat.pattern, - text: raw_mat.text, - hint: hint.to_string(), - }); - } - } else { - for raw_mat in raw_matches { - let hint = hints_iter - .next() - .expect("We should have as many hints as necessary, even invisible ones."); - - result.push(Match { - x: raw_mat.x, - y: raw_mat.y, - pattern: raw_mat.pattern, - text: raw_mat.text, - hint: hint.to_string(), - }); - } - } - - result - } - - /// Builds a `SequenceTrie` that helps determine if a sequence of keys - /// entered by the user corresponds to a match. This kind of lookup - /// directly returns a reference to the corresponding `Match` if any. - pub fn build_lookup_trie(matches: &'a Vec>) -> SequenceTrie { - let mut trie = SequenceTrie::new(); - - for (index, mat) in matches.iter().enumerate() { - let hint_chars = mat.hint.chars().collect::>(); - - // no need to insert twice the same hint - if trie.get(&hint_chars).is_none() { - trie.insert_owned(hint_chars, index); - } - } - - trie - } -} - -/// Represents matched text, its location on screen, the pattern that created -/// it, and the associated hint. -#[derive(Debug)] -pub struct Match<'a> { - pub x: i32, - pub y: i32, - pub pattern: &'a str, - pub text: &'a str, - pub hint: String, -} - -/// Internal surrogate for `Match`, before a Hint has been associated. -#[derive(Debug)] -struct RawMatch<'a> { - pub x: i32, - pub y: i32, - pub pattern: &'a str, - pub text: &'a str, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::alphabets::Alphabet; - - #[test] - fn match_reverse() { - let buffer = "lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 3); - assert_eq!(results.first().unwrap().hint, "a"); - assert_eq!(results.last().unwrap().hint, "c"); - } - - #[test] - fn match_unique() { - let buffer = "lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(true); - - assert_eq!(results.len(), 3); - assert_eq!(results.first().unwrap().hint, "a"); - assert_eq!(results.last().unwrap().hint, "a"); - } - - #[test] - fn match_docker() { - let buffer = "latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 1); - assert_eq!( - results.get(0).unwrap().text, - "30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4" - ); - } - - #[test] - fn match_ansi_colors() { - let buffer = "path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); - assert_eq!(results.get(1).unwrap().text, "test/log/nginx-2.log"); - assert_eq!(results.get(2).unwrap().text, "folder/.nginx@4df2.log"); - } - - #[test] - fn match_paths() { - let buffer = "Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text, "/tmp/foo/bar_lol"); - assert_eq!(results.get(1).unwrap().text, "/var/log/boot-strap.log"); - assert_eq!(results.get(2).unwrap().text, "../log/kern.log"); - } - - #[test] - fn match_home() { - let buffer = "Lorem ~/.gnu/.config.txt, lorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text, "~/.gnu/.config.txt"); - } - - #[test] - fn match_uuids() { - let buffer = "Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 1); - } - - #[test] - fn match_shas() { - let buffer = "Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text, "fd70b5695"); - assert_eq!(results.get(1).unwrap().text, "5246ddf"); - assert_eq!(results.get(2).unwrap().text, "f924213"); - assert_eq!( - results.get(3).unwrap().text, - "973113963b491874ab2e372ee60d4b4cb75f717c" - ); - } - - #[test] - fn match_ips() { - let buffer = "Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text, "127.0.0.1"); - assert_eq!(results.get(1).unwrap().text, "255.255.10.255"); - assert_eq!(results.get(2).unwrap().text, "127.0.0.1"); - } - - #[test] - fn match_ipv6s() { - let buffer = "Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text, "fe80::2:202:fe4"); - assert_eq!( - results.get(1).unwrap().text, - "2001:67c:670:202:7ba8:5e41:1591:d723" - ); - assert_eq!(results.get(2).unwrap().text, "fe80::2:1"); - assert_eq!(results.get(3).unwrap().text, "fe80:22:312:fe::1%eth0"); - } - - #[test] - fn match_markdown_urls() { - let buffer = - "Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 2); - assert_eq!(results.get(0).unwrap().pattern, "markdown_url"); - assert_eq!(results.get(0).unwrap().text, "https://github.io?foo=bar"); - assert_eq!(results.get(1).unwrap().pattern, "markdown_url"); - assert_eq!(results.get(1).unwrap().text, "http://cdn.com/img.jpg"); - } - - #[test] - fn match_urls() { - let buffer = "Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 4); - assert_eq!( - results.get(0).unwrap().text, - "https://www.rust-lang.org/tools" - ); - assert_eq!(results.get(0).unwrap().pattern, "url"); - assert_eq!(results.get(1).unwrap().text, "https://crates.io"); - assert_eq!(results.get(1).unwrap().pattern, "url"); - assert_eq!(results.get(2).unwrap().text, "https://github.io?foo=bar"); - assert_eq!(results.get(2).unwrap().pattern, "url"); - assert_eq!(results.get(3).unwrap().text, "ssh://github.io"); - assert_eq!(results.get(3).unwrap().pattern, "url"); - } - - #[test] - fn match_addresses() { - let buffer = "Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 3); - assert_eq!(results.get(0).unwrap().text, "0xfd70b5695"); - assert_eq!(results.get(1).unwrap().text, "0x5246ddf"); - assert_eq!(results.get(2).unwrap().text, "0x973113"); - } - - #[test] - fn match_hex_colors() { - let buffer = "Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 4); - assert_eq!(results.get(0).unwrap().text, "#fd7b56"); - assert_eq!(results.get(1).unwrap().text, "#FF00FF"); - assert_eq!(results.get(2).unwrap().text, "#00fF05"); - assert_eq!(results.get(3).unwrap().text, "#abcd00"); - } - - #[test] - fn match_ipfs() { - let buffer = "Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 1); - assert_eq!( - results.get(0).unwrap().text, - "QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ" - ); - } - - #[test] - fn match_process_port() { - let buffer = "Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 8); - } - - #[test] - fn match_diff_a() { - let buffer = "Lorem lorem\n--- a/src/main.rs"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text, "src/main.rs"); - } - - #[test] - fn match_diff_b() { - let buffer = "Lorem lorem\n+++ b/src/main.rs"; - let named_pat = vec![]; - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 1); - assert_eq!(results.get(0).unwrap().text, "src/main.rs"); - } - - #[test] - fn priority() { - let buffer = "Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"; - - let named_pat = vec![]; - let custom: Vec = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] - .iter() - .map(|&s| s.to_string()) - .collect(); - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 9); - assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); - assert_eq!(results.get(1).unwrap().text, "CUSTOM-52463"); - assert_eq!(results.get(2).unwrap().text, "ISSUE-123"); - assert_eq!(results.get(3).unwrap().text, "/var/fd70b569/9999.log"); - assert_eq!(results.get(4).unwrap().text, "52463"); - assert_eq!(results.get(5).unwrap().text, "973113"); - assert_eq!( - results.get(6).unwrap().text, - "123e4567-e89b-12d3-a456-426655440000" - ); - assert_eq!(results.get(7).unwrap().text, "8888"); - assert_eq!( - results.get(8).unwrap().text, - "https://crates.io/23456/fd70b569" - ); - } - - #[test] - fn named_patterns() { - let buffer = "Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"; - - use crate::regexes::parse_pattern_name; - let named_pat = vec![parse_pattern_name("url").unwrap()]; - - let custom = vec![]; - let alphabet = Alphabet("abcd".to_string()); - let results = Model::new(buffer, &alphabet, &named_pat, &custom, false).matches(false); - - assert_eq!(results.len(), 2); - assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); - assert_eq!( - results.get(1).unwrap().text, - "https://crates.io/23456/fd70b569" - ); - } -} diff --git a/src/process.rs b/src/process.rs deleted file mode 100644 index af6a5f1..0000000 --- a/src/process.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::process::Command; - -use crate::error::ParseError; - -/// Execute an arbitrary Unix command and return the stdout as a `String` if -/// successful. -pub fn execute(command: &str, args: &Vec<&str>) -> Result { - let output = Command::new(command).args(args).output()?; - - if !output.status.success() { - let msg = String::from_utf8_lossy(&output.stderr); - return Err(ParseError::ProcessFailure(format!( - "Process failure: {} {}, error {}", - command, - args.join(" "), - msg - ))); - } - - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} diff --git a/src/alphabets.rs b/src/textbuf/alphabet.rs similarity index 98% rename from src/alphabets.rs rename to src/textbuf/alphabet.rs index e1e7aaa..091583d 100644 --- a/src/alphabets.rs +++ b/src/textbuf/alphabet.rs @@ -6,7 +6,7 @@ use crate::error; /// /// Keep in mind letters 'n' and 'y' are systematically removed at runtime to /// prevent conflict with navigation and yank/copy keys. -const ALPHABETS: [(&'static str, &'static str); 21] = [ +const ALPHABETS: [(&str, &str); 21] = [ // ("abcd", "abcd"), ("qwerty", "asdfqwerzxcvjklmiuopghtybn"), ("qwerty-homerow", "asdfjklgh"), @@ -47,7 +47,7 @@ pub fn parse_alphabet(src: &str) -> Result { match alphabet_pair { Some((_name, letters)) => { let letters = letters.replace(&['n', 'N', 'y', 'Y'][..], ""); - Ok(Alphabet(letters.to_string())) + Ok(Alphabet(letters)) } None => Err(error::ParseError::UnknownAlphabet), } @@ -69,7 +69,7 @@ impl Alphabet { /// If more hints are needed, unfortunately, this will keep producing /// empty (`""`) hints. /// - /// ``` + /// ```text /// // The algorithm works as follows: /// // --- lead ---- /// // initial state | a b c d diff --git a/src/textbuf/matches.rs b/src/textbuf/matches.rs new file mode 100644 index 0000000..1695605 --- /dev/null +++ b/src/textbuf/matches.rs @@ -0,0 +1,10 @@ +/// Represents matched text, its location on screen, the pattern that created +/// it, and the associated hint. +#[derive(Debug)] +pub struct Match<'a> { + pub x: i32, + pub y: i32, + pub pattern: &'a str, + pub text: &'a str, + pub hint: String, +} diff --git a/src/textbuf/mod.rs b/src/textbuf/mod.rs new file mode 100644 index 0000000..af6abe6 --- /dev/null +++ b/src/textbuf/mod.rs @@ -0,0 +1,626 @@ +pub(crate) mod alphabet; +mod matches; +mod model; +mod raw_match; +pub(crate) mod regexes; + +pub use matches::Match; +pub use model::Model; + +#[cfg(test)] +mod tests { + use super::alphabet::Alphabet; + use super::model::Model; + + #[test] + fn match_reverse() { + let buffer = "lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 3); + assert_eq!(results.first().unwrap().hint, "a"); + assert_eq!(results.last().unwrap().hint, "c"); + } + + #[test] + fn match_unique() { + let buffer = "lorem 127.0.0.1 lorem 255.255.255.255 lorem 127.0.0.1 lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = true; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 3); + assert_eq!(results.first().unwrap().hint, "a"); + assert_eq!(results.last().unwrap().hint, "a"); + } + + #[test] + fn match_docker() { + let buffer = "latest sha256:30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4 20 hours ago"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 1); + assert_eq!( + results.get(0).unwrap().text, + "30557a29d5abc51e5f1d5b472e79b7e296f595abcf19fe6b9199dbbc809c6ff4" + ); + } + + #[test] + fn match_ansi_colors() { + let buffer = + "path: /var/log/nginx.log\npath: test/log/nginx-2.log:32folder/.nginx@4df2.log"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = true; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 3); + assert_eq!(results.get(0).unwrap().text, "/var/log/nginx.log"); + assert_eq!(results.get(1).unwrap().text, "test/log/nginx-2.log"); + assert_eq!(results.get(2).unwrap().text, "folder/.nginx@4df2.log"); + } + + #[test] + fn match_paths() { + let buffer = + "Lorem /tmp/foo/bar_lol, lorem\n Lorem /var/log/boot-strap.log lorem ../log/kern.log lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 3); + assert_eq!(results.get(0).unwrap().text, "/tmp/foo/bar_lol"); + assert_eq!(results.get(1).unwrap().text, "/var/log/boot-strap.log"); + assert_eq!(results.get(2).unwrap().text, "../log/kern.log"); + } + + #[test] + fn match_home() { + let buffer = "Lorem ~/.gnu/.config.txt, lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 1); + assert_eq!(results.get(0).unwrap().text, "~/.gnu/.config.txt"); + } + + #[test] + fn match_uuids() { + let buffer = "Lorem ipsum 123e4567-e89b-12d3-a456-426655440000 lorem\n Lorem lorem lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 1); + } + + #[test] + fn match_shas() { + let buffer = "Lorem fd70b5695 5246ddf f924213 lorem\n Lorem 973113963b491874ab2e372ee60d4b4cb75f717c lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 4); + assert_eq!(results.get(0).unwrap().text, "fd70b5695"); + assert_eq!(results.get(1).unwrap().text, "5246ddf"); + assert_eq!(results.get(2).unwrap().text, "f924213"); + assert_eq!( + results.get(3).unwrap().text, + "973113963b491874ab2e372ee60d4b4cb75f717c" + ); + } + + #[test] + fn match_ipv4s() { + let buffer = "Lorem ipsum 127.0.0.1 lorem\n Lorem 255.255.10.255 lorem 127.0.0.1 lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 3); + assert_eq!(results.get(0).unwrap().pattern, "ipv4"); + assert_eq!(results.get(0).unwrap().text, "127.0.0.1"); + assert_eq!(results.get(1).unwrap().pattern, "ipv4"); + assert_eq!(results.get(1).unwrap().text, "255.255.10.255"); + assert_eq!(results.get(2).unwrap().pattern, "ipv4"); + assert_eq!(results.get(2).unwrap().text, "127.0.0.1"); + } + + #[test] + fn match_ipv6s() { + let buffer = "Lorem ipsum fe80::2:202:fe4 lorem\n Lorem 2001:67c:670:202:7ba8:5e41:1591:d723 lorem fe80::2:1 lorem ipsum fe80:22:312:fe::1%eth0"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 4); + assert_eq!(results.get(0).unwrap().text, "fe80::2:202:fe4"); + assert_eq!( + results.get(1).unwrap().text, + "2001:67c:670:202:7ba8:5e41:1591:d723" + ); + assert_eq!(results.get(2).unwrap().text, "fe80::2:1"); + assert_eq!(results.get(3).unwrap().text, "fe80:22:312:fe::1%eth0"); + } + + #[test] + fn match_markdown_urls() { + let buffer = + "Lorem ipsum [link](https://github.io?foo=bar) ![](http://cdn.com/img.jpg) lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 2); + assert_eq!(results.get(0).unwrap().pattern, "markdown-url"); + assert_eq!(results.get(0).unwrap().text, "https://github.io?foo=bar"); + assert_eq!(results.get(1).unwrap().pattern, "markdown-url"); + assert_eq!(results.get(1).unwrap().text, "http://cdn.com/img.jpg"); + } + + #[test] + fn match_urls() { + let buffer = "Lorem ipsum https://www.rust-lang.org/tools lorem\n Lorem ipsumhttps://crates.io lorem https://github.io?foo=bar lorem ssh://github.io"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 4); + assert_eq!( + results.get(0).unwrap().text, + "https://www.rust-lang.org/tools" + ); + assert_eq!(results.get(0).unwrap().pattern, "url"); + assert_eq!(results.get(1).unwrap().text, "https://crates.io"); + assert_eq!(results.get(1).unwrap().pattern, "url"); + assert_eq!(results.get(2).unwrap().text, "https://github.io?foo=bar"); + assert_eq!(results.get(2).unwrap().pattern, "url"); + assert_eq!(results.get(3).unwrap().text, "ssh://github.io"); + assert_eq!(results.get(3).unwrap().pattern, "url"); + } + + #[test] + fn match_emails() { + let buffer = + "Lorem ipsum john@server.department.company.com lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 2); + assert_eq!(results.get(0).unwrap().pattern, "email"); + assert_eq!( + results.get(0).unwrap().text, + "first.last+social@example.com" + ); + assert_eq!(results.get(1).unwrap().pattern, "email"); + assert_eq!( + results.get(1).unwrap().text, + "john@server.department.company.com" + ); + } + + #[test] + fn match_addresses() { + let buffer = "Lorem 0xfd70b5695 0x5246ddf lorem\n Lorem 0x973113tlorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 3); + assert_eq!(results.get(0).unwrap().pattern, "mem-address"); + assert_eq!(results.get(0).unwrap().text, "0xfd70b5695"); + assert_eq!(results.get(1).unwrap().pattern, "mem-address"); + assert_eq!(results.get(1).unwrap().text, "0x5246ddf"); + assert_eq!(results.get(2).unwrap().pattern, "mem-address"); + assert_eq!(results.get(2).unwrap().text, "0x973113"); + } + + #[test] + fn match_hex_colors() { + let buffer = "Lorem #fd7b56 lorem #FF00FF\n Lorem #00fF05 lorem #abcd00 lorem #afRR00"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 4); + assert_eq!(results.get(0).unwrap().text, "#fd7b56"); + assert_eq!(results.get(1).unwrap().text, "#FF00FF"); + assert_eq!(results.get(2).unwrap().text, "#00fF05"); + assert_eq!(results.get(3).unwrap().text, "#abcd00"); + } + + #[test] + fn match_ipfs() { + let buffer = "Lorem QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ lorem Qmfoobar"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 1); + assert_eq!( + results.get(0).unwrap().text, + "QmRdbNSxDJBXmssAc9fvTtux4duptMvfSGiGuq6yHAQVKQ" + ); + } + + #[test] + fn match_process_port() { + let buffer = "Lorem 5695 52463 lorem\n Lorem 973113 lorem 99999 lorem 8888 lorem\n 23456 lorem 5432 lorem 23444"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 8); + } + + #[test] + fn match_diff_a() { + let buffer = "Lorem lorem\n--- a/src/main.rs"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 1); + assert_eq!(results.get(0).unwrap().pattern, "diff-a"); + assert_eq!(results.get(0).unwrap().text, "src/main.rs"); + } + + #[test] + fn match_diff_b() { + let buffer = "Lorem lorem\n+++ b/src/main.rs"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 1); + assert_eq!(results.get(0).unwrap().pattern, "diff-b"); + assert_eq!(results.get(0).unwrap().text, "src/main.rs"); + } + + #[test] + fn priority_between_regexes() { + let buffer = "Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; + let named_pat = vec![]; + let custom: Vec = ["CUSTOM-[0-9]{4,}", "ISSUE-[0-9]{3}"] + .iter() + .map(|&s| s.to_string()) + .collect(); + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 9); + assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); + assert_eq!(results.get(1).unwrap().text, "CUSTOM-52463"); + assert_eq!(results.get(2).unwrap().text, "ISSUE-123"); + assert_eq!(results.get(3).unwrap().text, "/var/fd70b569/9999.log"); + assert_eq!(results.get(4).unwrap().text, "52463"); + assert_eq!(results.get(5).unwrap().text, "973113"); + assert_eq!( + results.get(6).unwrap().text, + "123e4567-e89b-12d3-a456-426655440000" + ); + assert_eq!(results.get(7).unwrap().text, "8888"); + assert_eq!( + results.get(8).unwrap().text, + "https://crates.io/23456/fd70b569" + ); + } + + #[test] + fn named_patterns() { + let buffer = "Lorem [link](http://foo.bar) ipsum CUSTOM-52463 lorem ISSUE-123 lorem\nLorem /var/fd70b569/9999.log 52463 lorem\n Lorem 973113 lorem 123e4567-e89b-12d3-a456-426655440000 lorem 8888 lorem\n https://crates.io/23456/fd70b569 lorem"; + let lines = buffer.split('\n').collect::>(); + + let use_all_patterns = false; + use crate::textbuf::regexes::parse_pattern_name; + let named_pat = vec![parse_pattern_name("url").unwrap()]; + + let custom = vec![]; + let alphabet = Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let results = Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom, + reverse, + unique_hint, + ) + .matches; + + assert_eq!(results.len(), 2); + assert_eq!(results.get(0).unwrap().text, "http://foo.bar"); + assert_eq!( + results.get(1).unwrap().text, + "https://crates.io/23456/fd70b569" + ); + } +} diff --git a/src/textbuf/model.rs b/src/textbuf/model.rs new file mode 100644 index 0000000..9208e35 --- /dev/null +++ b/src/textbuf/model.rs @@ -0,0 +1,239 @@ +use std::collections; + +use regex::Regex; +use sequence_trie::SequenceTrie; + +use super::alphabet::Alphabet; +use super::matches::Match; +use super::raw_match::RawMatch; +use super::regexes::{NamedPattern, EXCLUDE_PATTERNS, PATTERNS}; + +/// Holds data for the `Ui`. +pub struct Model<'a> { + // buffer: &'a str, + pub lines: &'a [&'a str], + pub reverse: bool, + pub matches: Vec>, + pub lookup_trie: SequenceTrie, +} + +impl<'a> Model<'a> { + pub fn new( + // buffer: &'a str, + lines: &'a [&'a str], + alphabet: &'a Alphabet, + use_all_patterns: bool, + named_patterns: &'a [NamedPattern], + custom_patterns: &'a [String], + reverse: bool, + unique_hint: bool, + ) -> Model<'a> { + // let lines = buffer.split('\n').collect::>(); + + let mut raw_matches = + raw_matches(&lines, named_patterns, custom_patterns, use_all_patterns); + + if reverse { + raw_matches.reverse(); + } + + let mut matches = associate_hints(&raw_matches, alphabet, unique_hint); + + if reverse { + matches.reverse(); + } + + let lookup_trie = build_lookup_trie(&matches); + + Model { + // buffer, + lines, + reverse, + matches, + lookup_trie, + } + } +} + +/// Internal function that searches the model's lines for pattern matches. +/// Returns a vector of `RawMatch`es (text, location, pattern id) without +/// an associated hint. The hint is attached to `Match`, not to `RawMatch`. +/// +/// # Notes +/// +/// Custom regexes have priority over other regexes. +/// +/// If no named patterns were specified, it will search for all available +/// patterns from the `PATTERNS` catalog. +fn raw_matches<'a>( + lines: &'a [&'a str], + named_patterns: &'a [NamedPattern], + custom_patterns: &'a [String], + use_all_patterns: bool, +) -> Vec> { + let exclude_regexes = EXCLUDE_PATTERNS + .iter() + .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) + .collect::>(); + + let custom_regexes = custom_patterns + .iter() + .map(|pattern| { + ( + "custom", + Regex::new(pattern).expect("Invalid custom regexp"), + ) + }) + .collect::>(); + + let regexes = if use_all_patterns { + PATTERNS + .iter() + .map(|&(name, pattern)| (name, Regex::new(pattern).unwrap())) + .collect::>() + } else { + named_patterns + .iter() + .map(|NamedPattern(name, pattern)| (name.as_str(), Regex::new(pattern).unwrap())) + .collect::>() + }; + + let all_regexes = [exclude_regexes, custom_regexes, regexes].concat(); + + let mut raw_matches = Vec::new(); + + for (index, line) in lines.iter().enumerate() { + // Chunk is the remainder of the line to be searched for matches. + // This advances iteratively, until no matches can be found. + let mut chunk: &str = line; + let mut offset: i32 = 0; + + // Use all avail regexes to match the chunk and select the match + // occuring the earliest on the chunk. Save its matched text and + // position in a `RawMatch` struct. + loop { + // For each avalable regex, use the `find_iter` iterator to + // get the first non-overlapping match in the chunk, returning + // the start and end byte indices with respect to the chunk. + let chunk_matches = all_regexes + .iter() + .filter_map(|(&ref pat_name, reg)| match reg.find_iter(chunk).next() { + Some(reg_match) => Some((pat_name, reg, reg_match)), + None => None, + }) + .collect::>(); + + if chunk_matches.is_empty() { + break; + } + + // First match on the chunk. + let (pat_name, reg, reg_match) = chunk_matches + .iter() + .min_by_key(|element| element.2.start()) + .unwrap(); + + // Never hint or break ansi color sequences. + if *pat_name != "ansi_colors" { + let text = reg_match.as_str(); + + // In case the pattern has a capturing group, try obtaining + // that text and start offset, else use the entire match. + let (subtext, substart) = match reg + .captures_iter(text) + .next() + .expect("This regex is guaranteed to match.") + .get(1) + { + Some(capture) => (capture.as_str(), capture.start()), + None => (text, 0), + }; + + raw_matches.push(RawMatch { + x: offset + reg_match.start() as i32 + substart as i32, + y: index as i32, + pattern: pat_name, + text: subtext, + }); + } + + chunk = chunk + .get(reg_match.end()..) + .expect("The chunk must be larger than the regex match."); + offset += reg_match.end() as i32; + } + } + + raw_matches +} + +/// Associate a hint to each `RawMatch`, returning a vector of `Match`es. +/// +/// If `unique` is `true`, all duplicate matches will have the same hint. +/// For copying matched text, this seems easier and more natural. +/// If `unique` is `false`, duplicate matches will have their own hint. +fn associate_hints<'a>( + raw_matches: &[RawMatch<'a>], + alphabet: &'a Alphabet, + unique: bool, +) -> Vec> { + let hints = alphabet.make_hints(raw_matches.len()); + let mut hints_iter = hints.iter(); + + let mut result: Vec> = vec![]; + + if unique { + // Map (text, hint) + let mut known: collections::HashMap<&str, &str> = collections::HashMap::new(); + + for raw_mat in raw_matches { + let hint: &str = known.entry(raw_mat.text).or_insert_with(|| { + hints_iter + .next() + .expect("We should have as many hints as necessary, even invisible ones.") + }); + + result.push(Match { + x: raw_mat.x, + y: raw_mat.y, + pattern: raw_mat.pattern, + text: raw_mat.text, + hint: hint.to_string(), + }); + } + } else { + for raw_mat in raw_matches { + let hint = hints_iter + .next() + .expect("We should have as many hints as necessary, even invisible ones."); + + result.push(Match { + x: raw_mat.x, + y: raw_mat.y, + pattern: raw_mat.pattern, + text: raw_mat.text, + hint: hint.to_string(), + }); + } + } + + result +} + +/// Builds a `SequenceTrie` that helps determine if a sequence of keys +/// entered by the user corresponds to a match. This kind of lookup +/// directly returns a reference to the corresponding `Match` if any. +fn build_lookup_trie<'a>(matches: &'a [Match<'a>]) -> SequenceTrie { + let mut trie = SequenceTrie::new(); + + for (index, mat) in matches.iter().enumerate() { + let hint_chars = mat.hint.chars().collect::>(); + + // no need to insert twice the same hint + if trie.get(&hint_chars).is_none() { + trie.insert_owned(hint_chars, index); + } + } + + trie +} diff --git a/src/textbuf/raw_match.rs b/src/textbuf/raw_match.rs new file mode 100644 index 0000000..7da45ce --- /dev/null +++ b/src/textbuf/raw_match.rs @@ -0,0 +1,8 @@ +/// Internal surrogate for `Match`, before a Hint has been associated. +#[derive(Debug)] +pub(super) struct RawMatch<'a> { + pub x: i32, + pub y: i32, + pub pattern: &'a str, + pub text: &'a str, +} diff --git a/src/regexes.rs b/src/textbuf/regexes.rs similarity index 54% rename from src/regexes.rs rename to src/textbuf/regexes.rs index cca6679..6b823b5 100644 --- a/src/regexes.rs +++ b/src/textbuf/regexes.rs @@ -1,16 +1,21 @@ use crate::error; -pub const EXCLUDE_PATTERNS: [(&'static str, &'static str); 1] = +pub(super) const EXCLUDE_PATTERNS: [(&str, &str); 1] = [("ansi_colors", r"[[:cntrl:]]\[([0-9]{1,2};)?([0-9]{1,2})?m")]; -pub const PATTERNS: [(&'static str, &'static str); 14] = [ - ("markdown_url", r"\[[^]]*\]\(([^)]+)\)"), +/// Holds all the regex patterns that are currently supported. +/// +/// The email address was obtained at https://www.regular-expressions.info/email.html. +/// Others were obtained from Ferran Basora. +pub(super) const PATTERNS: [(&str, &str); 16] = [ + ("markdown-url", r"\[[^]]*\]\(([^)]+)\)"), ( "url", r"((https?://|git@|git://|ssh://|ftp://|file:///)[^ \(\)\[\]\{\}]+)", ), - ("diff_a", r"--- a/([^ ]+)"), - ("diff_b", r"\+\+\+ b/([^ ]+)"), + ("email", r"\b[A-z0-9._%+-]+@[A-z0-9.-]+\.[A-z]{2,}\b"), + ("diff-a", r"--- a/([^ ]+)"), + ("diff-b", r"\+\+\+ b/([^ ]+)"), ("docker", r"sha256:([0-9a-f]{64})"), ("path", r"(([.\w\-@~]+)?(/[.\w\-@]+)+)"), ("hexcolor", r"#[0-9a-fA-F]{6}"), @@ -18,12 +23,16 @@ pub const PATTERNS: [(&'static str, &'static str); 14] = [ "uuid", r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", ), + ( + "version", + r"(v?\d{1,4}\.\d{1,4}(\.\d{1,4})?(-(alpha|beta|rc)(\.\d)?)?)[^.0-9s]", + ), ("ipfs", r"Qm[0-9a-zA-Z]{44}"), ("sha", r"[0-9a-f]{7,40}"), - ("ip", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"), + ("ipv4", r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"), ("ipv6", r"[A-f0-9:]+:+[A-f0-9:]+[%\w\d]+"), - ("address", r"0x[0-9a-fA-F]+"), - ("number", r"[0-9]{4,}"), + ("mem-address", r"0x[0-9a-fA-F]+"), + ("digits", r"[0-9]{4,}"), ]; /// Type-safe string Pattern Name (newtype). @@ -31,7 +40,7 @@ pub const PATTERNS: [(&'static str, &'static str); 14] = [ pub struct NamedPattern(pub String, pub String); /// Parse a name string into `NamedPattern`, used during CLI parsing. -pub fn parse_pattern_name(src: &str) -> Result { +pub(crate) fn parse_pattern_name(src: &str) -> Result { match PATTERNS.iter().find(|&(name, _pattern)| name == &src) { Some((name, pattern)) => Ok(NamedPattern(name.to_string(), pattern.to_string())), None => Err(error::ParseError::UnknownPatternName), diff --git a/src/tmux.rs b/src/tmux.rs index acff280..b87375a 100644 --- a/src/tmux.rs +++ b/src/tmux.rs @@ -1,11 +1,15 @@ -use clap::Clap; +//! This module provides types and functions to use Tmux. +//! +//! The main use cases are running Tmux commands & parsing Tmux panes +//! information. + use regex::Regex; use std::collections::HashMap; use std::fmt; use std::str::FromStr; -use copyrat::error::ParseError; -use copyrat::process; +use crate::config::extended::CaptureRegion; +use crate::error::ParseError; #[derive(Debug, PartialEq)] pub struct Pane { @@ -109,34 +113,6 @@ impl fmt::Display for PaneId { } } -#[derive(Clap, Debug)] -pub enum CaptureRegion { - /// The entire history. - /// - /// This will end up sending `-S - -E -` to `tmux capture-pane`. - EntireHistory, - /// The visible area. - VisibleArea, - ///// Region from start line to end line - ///// - ///// This works as defined in tmux's docs (order does not matter). - //Region(i32, i32), -} - -impl FromStr for CaptureRegion { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match s { - "leading" => Ok(CaptureRegion::EntireHistory), - "trailing" => Ok(CaptureRegion::VisibleArea), - _ => Err(ParseError::ExpectedString(String::from( - "entire-history or visible-area", - ))), - } - } -} - /// Returns a list of `Pane` from the current tmux session. pub fn list_panes() -> Result, ParseError> { let args = vec![ @@ -145,7 +121,7 @@ pub fn list_panes() -> Result, ParseError> { "#{pane_id}:#{?pane_in_mode,true,false}:#{pane_height}:#{scroll_position}:#{?pane_active,true,false}", ]; - let output = process::execute("tmux", &args)?; + let output = duct::cmd("tmux", &args).read()?; // Each call to `Pane::parse` returns a `Result`. All results // are collected into a Result, _>, thanks to `collect()`. @@ -165,9 +141,7 @@ pub fn list_panes() -> Result, ParseError> { /// # Example /// ```get_options("@copyrat-")``` pub fn get_options(prefix: &str) -> Result, ParseError> { - let args = vec!["show", "-g"]; - - let output = process::execute("tmux", &args)?; + let output = duct::cmd!("tmux", "show", "-g").read()?; let lines: Vec<&str> = output.split('\n').collect(); let pattern = format!(r#"{prefix}([\w\-0-9]+) "?(\w+)"?"#, prefix = prefix); @@ -190,8 +164,8 @@ pub fn get_options(prefix: &str) -> Result, ParseError> /// Returns the entire Pane content as a `String`. /// -/// `CaptureRegion` specifies if the visible area is captured, or the entire -/// history. +/// The provided `region` specifies if the visible area is captured, or the +/// entire history. /// /// # TODO /// @@ -223,16 +197,14 @@ pub fn capture_pane(pane: &Pane, region: &CaptureRegion) -> Result = args.split(' ').collect(); - let output = process::execute("tmux", &args)?; + let output = duct::cmd("tmux", &args).read()?; Ok(output) } /// Ask tmux to swap the current Pane with the target_pane (uses Tmux format). pub fn swap_pane_with(target_pane: &str) -> Result<(), ParseError> { // -Z: keep the window zoomed if it was zoomed. - let args = vec!["swap-pane", "-Z", "-s", target_pane]; - - process::execute("tmux", &args)?; + duct::cmd!("tmux", "swap-pane", "-Z", "-s", target_pane).run()?; Ok(()) } @@ -241,7 +213,7 @@ pub fn swap_pane_with(target_pane: &str) -> Result<(), ParseError> { mod tests { use super::Pane; use super::PaneId; - use copyrat::error; + use crate::error; use std::str::FromStr; #[test] diff --git a/src/ui/colors.rs b/src/ui/colors.rs new file mode 100644 index 0000000..daa8674 --- /dev/null +++ b/src/ui/colors.rs @@ -0,0 +1,95 @@ +use crate::error; +use clap::Clap; +use termion::color; + +pub fn parse_color(src: &str) -> Result, error::ParseError> { + match src { + "black" => Ok(Box::new(color::Black)), + "red" => Ok(Box::new(color::Red)), + "green" => Ok(Box::new(color::Green)), + "yellow" => Ok(Box::new(color::Yellow)), + "blue" => Ok(Box::new(color::Blue)), + "magenta" => Ok(Box::new(color::Magenta)), + "cyan" => Ok(Box::new(color::Cyan)), + "white" => Ok(Box::new(color::White)), + "bright-black" => Ok(Box::new(color::LightBlack)), + "bright-red" => Ok(Box::new(color::LightRed)), + "bright-green" => Ok(Box::new(color::LightGreen)), + "bright-yellow" => Ok(Box::new(color::LightYellow)), + "bright-blue" => Ok(Box::new(color::LightBlue)), + "bright-magenta" => Ok(Box::new(color::LightMagenta)), + "bright-cyan" => Ok(Box::new(color::LightCyan)), + "bright-white" => Ok(Box::new(color::LightWhite)), + // "default" => Ok(Box::new(color::Reset)), + _ => Err(error::ParseError::UnknownColor), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn match_color() { + let text1 = format!( + "{}{}", + color::Fg(parse_color("green").unwrap().as_ref()), + "foo" + ); + let text2 = format!("{}{}", color::Fg(color::Green), "foo"); + + assert_eq!(text1, text2); + } + + #[test] + fn no_match_color() { + assert!(parse_color("wat").is_err(), "this color should not exist"); + } +} + +/// Holds color-related data. +/// +/// - `focus_*` colors are used to render the currently focused matched text. +/// - `normal_*` colors are used to render other matched text. +/// - `hint_*` colors are used to render the hints. +#[derive(Clap, Debug)] +#[clap(about)] // Needed to avoid this doc comment to be used as overall `about`. +pub struct UiColors { + /// Foreground color for base text. + #[clap(long, default_value = "bright-cyan", parse(try_from_str = parse_color))] + pub text_fg: Box, + + /// Background color for base text. + #[clap(long, default_value = "bright-white", parse(try_from_str = parse_color))] + pub text_bg: Box, + + /// Foreground color for matches. + #[clap(long, default_value = "yellow", + parse(try_from_str = parse_color))] + pub match_fg: Box, + + /// Background color for matches. + #[clap(long, default_value = "bright-white", + parse(try_from_str = parse_color))] + pub match_bg: Box, + + /// Foreground color for the focused match. + #[clap(long, default_value = "magenta", + parse(try_from_str = parse_color))] + pub focused_fg: Box, + + /// Background color for the focused match. + #[clap(long, default_value = "bright-white", + parse(try_from_str = parse_color))] + pub focused_bg: Box, + + /// Foreground color for hints. + #[clap(long, default_value = "white", + parse(try_from_str = parse_color))] + pub hint_fg: Box, + + /// Background color for hints. + #[clap(long, default_value = "magenta", + parse(try_from_str = parse_color))] + pub hint_bg: Box, +} diff --git a/src/ui/hint_alignment.rs b/src/ui/hint_alignment.rs new file mode 100644 index 0000000..610bdb5 --- /dev/null +++ b/src/ui/hint_alignment.rs @@ -0,0 +1,26 @@ +use clap::Clap; +use std::str::FromStr; + +use crate::error::ParseError; + +/// Describes if, during rendering, a hint should aligned to the leading edge of +/// the matched text, or to its trailing edge. +#[derive(Debug, Clap)] +pub enum HintAlignment { + Leading, + Trailing, +} + +impl FromStr for HintAlignment { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match s { + "leading" => Ok(HintAlignment::Leading), + "trailing" => Ok(HintAlignment::Trailing), + _ => Err(ParseError::ExpectedString(String::from( + "leading or trailing", + ))), + } + } +} diff --git a/src/ui/hint_style.rs b/src/ui/hint_style.rs new file mode 100644 index 0000000..84cf943 --- /dev/null +++ b/src/ui/hint_style.rs @@ -0,0 +1,15 @@ +/// Describes the style of contrast to be used during rendering of the hint's +/// text. +/// +/// # Note +/// In practice, this is wrapped in an `Option`, so that the hint's text can be rendered with no style. +pub enum HintStyle { + /// The hint's text will be bold (leveraging `termion::style::Bold`). + Bold, + /// The hint's text will be italicized (leveraging `termion::style::Italic`). + Italic, + /// The hint's text will be underlined (leveraging `termion::style::Underline`). + Underline, + /// The hint's text will be surrounded by these chars. + Surround(char, char), +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..2db4d8d --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,29 @@ +//! The `ui` module is responsible for presenting information to the user and +//! handling keypresses. +//! +//! In particular, the `Ui` struct +//! +//! - renders text, matched text and hints from the structured buffer content +//! to the screen, +//! - listens for keypress events, +//! - and returns the user selection in the form of a `Selection` struct. +//! +//! Via keypresses the user can +//! +//! - navigate the buffer (in case it is larger than the number of lines in +//! the terminal) +//! - move the focus from one match to another +//! - select one of the matches +//! - toggle the output destination (tmux buffer or clipboard) +//! + +pub mod colors; +pub mod hint_alignment; +pub mod hint_style; +mod selection; +mod vc; + +pub use hint_alignment::HintAlignment; +pub use hint_style::HintStyle; +pub use selection::Selection; +pub use vc::ViewController; diff --git a/src/ui/selection.rs b/src/ui/selection.rs new file mode 100644 index 0000000..042016a --- /dev/null +++ b/src/ui/selection.rs @@ -0,0 +1,9 @@ +use crate::config::extended::OutputDestination; + +/// Represents the text selected by the user, along with if it was uppercased +/// and the output destination (Tmux buffer or Clipboard). +pub struct Selection { + pub text: String, + pub uppercased: bool, + pub output_destination: OutputDestination, +} diff --git a/src/ui.rs b/src/ui/vc.rs similarity index 82% rename from src/ui.rs rename to src/ui/vc.rs index 196cd3f..6dcfc24 100644 --- a/src/ui.rs +++ b/src/ui/vc.rs @@ -1,57 +1,62 @@ use std::char; +use std::cmp; use std::io; -use std::str::FromStr; -use clap::Clap; -use sequence_trie::SequenceTrie; use termion::{self, color, cursor, event, style}; -use crate::error::ParseError; -use crate::{colors, model}; +use super::colors::UiColors; +use super::Selection; +use super::{HintAlignment, HintStyle}; +use crate::{config::extended::OutputDestination, textbuf}; -pub struct Ui<'a> { - model: &'a mut model::Model<'a>, +pub struct ViewController<'a> { + model: &'a textbuf::Model<'a>, term_width: u16, line_offsets: Vec, - matches: Vec>, - lookup_trie: SequenceTrie, focus_index: usize, focus_wrap_around: bool, + default_output_destination: OutputDestination, rendering_colors: &'a UiColors, hint_alignment: &'a HintAlignment, hint_style: Option, } -impl<'a> Ui<'a> { +impl<'a> ViewController<'a> { + // Initialize {{{1 + pub fn new( - model: &'a mut model::Model<'a>, - unique_hint: bool, + model: &'a textbuf::Model<'a>, focus_wrap_around: bool, + default_output_destination: OutputDestination, rendering_colors: &'a UiColors, hint_alignment: &'a HintAlignment, hint_style: Option, - ) -> Ui<'a> { - let matches = model.matches(unique_hint); - let lookup_trie = model::Model::build_lookup_trie(&matches); - let focus_index = if model.reverse { matches.len() - 1 } else { 0 }; + ) -> ViewController<'a> { + let focus_index = if model.reverse { + model.matches.len() - 1 + } else { + 0 + }; let (term_width, _) = termion::terminal_size().unwrap_or((80u16, 30u16)); // .expect("Cannot read the terminal size."); let line_offsets = get_line_offsets(&model.lines, term_width); - Ui { + ViewController { model, term_width, line_offsets, - matches, - lookup_trie, focus_index, focus_wrap_around, + default_output_destination, rendering_colors, hint_alignment, hint_style, } } + // }}} + // Coordinates {{{1 + /// Convert the `Match` text into the coordinates of the wrapped lines. /// /// Compute the new x offset of the text as the remainder of the line width @@ -61,7 +66,7 @@ impl<'a> Ui<'a> { /// Compute the new y offset of the text as the initial y offset plus any /// additional offset due to previous split lines. This is obtained thanks to /// the `offset_per_line` member. - pub fn map_coords_to_wrapped_space(&self, offset_x: usize, offset_y: usize) -> (usize, usize) { + fn map_coords_to_wrapped_space(&self, offset_x: usize, offset_y: usize) -> (usize, usize) { let line_width = self.term_width as usize; let new_offset_x = offset_x % line_width; @@ -71,51 +76,13 @@ impl<'a> Ui<'a> { (new_offset_x, new_offset_y) } - /// Move focus onto the previous hint, returning both the index of the - /// previously focused match, and the index of the newly focused one. - fn prev_focus_index(&mut self) -> (usize, usize) { - let old_index = self.focus_index; - if self.focus_wrap_around { - if self.focus_index == 0 { - self.focus_index = self.matches.len() - 1; - } else { - self.focus_index -= 1; - } - } else { - if self.focus_index > 0 { - self.focus_index -= 1; - } - } - let new_index = self.focus_index; - (old_index, new_index) - } - - /// Move focus onto the next hint, returning both the index of the - /// previously focused match, and the index of the newly focused one. - fn next_focus_index(&mut self) -> (usize, usize) { - let old_index = self.focus_index; - if self.focus_wrap_around { - if self.focus_index == self.matches.len() - 1 { - self.focus_index = 0; - } else { - self.focus_index += 1; - } - } else { - if self.focus_index < self.matches.len() - 1 { - self.focus_index += 1; - } - } - let new_index = self.focus_index; - (old_index, new_index) - } - /// Returns screen offset of a given `Match`. /// /// If multibyte characters occur before the hint (in the "prefix"), then /// their compouding takes less space on screen when printed: for /// instance ´ + e = é. Consequently the hint offset has to be adjusted /// to the left. - fn match_offsets(&self, mat: &model::Match<'a>) -> (usize, usize) { + fn match_offsets(&self, mat: &textbuf::Match<'a>) -> (usize, usize) { let offset_x = { let line = &self.model.lines[mat.y as usize]; let prefix = &line[0..mat.x as usize]; @@ -127,6 +94,46 @@ impl<'a> Ui<'a> { (offset_x, offset_y) } + // }}} + // Focus management {{{1 + + /// Move focus onto the previous hint, returning both the index of the + /// previously focused match, and the index of the newly focused one. + fn prev_focus_index(&mut self) -> (usize, usize) { + let old_index = self.focus_index; + if self.focus_wrap_around { + if self.focus_index == 0 { + self.focus_index = self.model.matches.len() - 1; + } else { + self.focus_index -= 1; + } + } else if self.focus_index > 0 { + self.focus_index -= 1; + } + let new_index = self.focus_index; + (old_index, new_index) + } + + /// Move focus onto the next hint, returning both the index of the + /// previously focused match, and the index of the newly focused one. + fn next_focus_index(&mut self) -> (usize, usize) { + let old_index = self.focus_index; + if self.focus_wrap_around { + if self.focus_index == self.model.matches.len() - 1 { + self.focus_index = 0; + } else { + self.focus_index += 1; + } + } else if self.focus_index < self.model.matches.len() - 1 { + self.focus_index += 1; + } + let new_index = self.focus_index; + (old_index, new_index) + } + + // }}} + // Rendering {{{1 + /// Render entire model lines on provided writer. /// /// This renders the basic content on which matches and hints can be rendered. @@ -136,10 +143,10 @@ impl<'a> Ui<'a> { /// - This writes directly on the writer, avoiding extra allocation. fn render_base_text( stdout: &mut dyn io::Write, - lines: &Vec<&str>, - line_offsets: &Vec, + lines: &[&str], + line_offsets: &[usize], colors: &UiColors, - ) -> () { + ) { write!( stdout, "{bg_color}{fg_color}", @@ -313,13 +320,13 @@ impl<'a> Ui<'a> { /// Convenience function that renders both the matched text and its hint, /// if focused. - fn render_match(&self, stdout: &mut dyn io::Write, mat: &model::Match<'a>, focused: bool) { + fn render_match(&self, stdout: &mut dyn io::Write, mat: &textbuf::Match<'a>, focused: bool) { let text = mat.text; let (offset_x, offset_y) = self.match_offsets(mat); let (offset_x, offset_y) = self.map_coords_to_wrapped_space(offset_x, offset_y); - Ui::render_matched_text( + ViewController::render_matched_text( stdout, text, focused, @@ -336,7 +343,7 @@ impl<'a> Ui<'a> { HintAlignment::Trailing => text.len() - mat.hint.len(), }; - Ui::render_matched_hint( + ViewController::render_matched_hint( stdout, &mat.hint, (offset_x + extra_offset, offset_y), @@ -360,16 +367,16 @@ impl<'a> Ui<'a> { /// # Note /// Multibyte characters are taken into account, so that the Match's `text` /// and `hint` are rendered in their proper position. - fn full_render(&self, stdout: &mut dyn io::Write) -> () { + fn full_render(&self, stdout: &mut dyn io::Write) { // 1. Trim all lines and render non-empty ones. - Ui::render_base_text( + ViewController::render_base_text( stdout, &self.model.lines, &self.line_offsets, &self.rendering_colors, ); - for (index, mat) in self.matches.iter().enumerate() { + for (index, mat) in self.model.matches.iter().enumerate() { let focused = index == self.focus_index; self.render_match(stdout, mat, focused); } @@ -386,18 +393,21 @@ impl<'a> Ui<'a> { new_focus_index: usize, ) { // Render the previously focused match as non-focused - let mat = self.matches.get(old_focus_index).unwrap(); + let mat = self.model.matches.get(old_focus_index).unwrap(); let focused = false; self.render_match(stdout, mat, focused); // Render the previously focused match as non-focused - let mat = self.matches.get(new_focus_index).unwrap(); + let mat = self.model.matches.get(new_focus_index).unwrap(); let focused = true; self.render_match(stdout, mat, focused); stdout.flush().unwrap(); } + // }}} + // Listening {{{1 + /// Listen to keys entered on stdin, moving focus accordingly, or /// selecting one match. /// @@ -407,11 +417,13 @@ impl<'a> Ui<'a> { fn listen(&mut self, reader: &mut dyn io::Read, writer: &mut dyn io::Write) -> Event { use termion::input::TermRead; // Trait for `reader.keys().next()`. - if self.matches.is_empty() { + if self.model.matches.is_empty() { return Event::Exit; } let mut typed_hint = String::new(); + let mut uppercased = false; + let mut output_destination = self.default_output_destination.clone(); self.full_render(writer); @@ -471,30 +483,52 @@ impl<'a> Ui<'a> { } // Yank/copy - event::Key::Char(_ch @ 'y') => { - let text = self.matches.get(self.focus_index).unwrap().text; - return Event::Match((text.to_string(), false)); + event::Key::Char(_ch @ 'y') | event::Key::Char(_ch @ '\n') => { + let text = self.model.matches.get(self.focus_index).unwrap().text; + return Event::Match(Selection { + text: text.to_string(), + uppercased: false, + output_destination, + }); } event::Key::Char(_ch @ 'Y') => { - let text = self.matches.get(self.focus_index).unwrap().text; - return Event::Match((text.to_string(), true)); + let text = self.model.matches.get(self.focus_index).unwrap().text; + return Event::Match(Selection { + text: text.to_string(), + uppercased: true, + output_destination, + }); } - // TODO: use a Trie or another data structure to determine + event::Key::Char(_ch @ ' ') => { + output_destination.toggle(); + let message = format!("output destination: `{}`", output_destination); + duct::cmd!("tmux", "display-message", &message) + .run() + .expect("could not make tmux display the message."); + continue; + } + + // Use a Trie or another data structure to determine // if the entered key belongs to a longer hint. // Attempts at finding a match with a corresponding hint. + // + // If any of the typed character is caps, the typed hint is + // deemed as uppercased. event::Key::Char(ch) => { let key = ch.to_string(); let lower_key = key.to_lowercase(); + uppercased = uppercased || (key != lower_key); typed_hint.push_str(&lower_key); let node = self + .model .lookup_trie .get_node(&typed_hint.chars().collect::>()); if node.is_none() { - // An unknown key was entered. + // A key outside the alphabet was entered. return Event::Exit; } @@ -504,10 +538,13 @@ impl<'a> Ui<'a> { let match_index = node.value().expect( "By construction, the Lookup Trie should have a value for each leaf.", ); - let mat = self.matches.get(*match_index).expect("By construction, the value in a leaf should correspond to an existing hint."); + let mat = self.model.matches.get(*match_index).expect("By construction, the value in a leaf should correspond to an existing hint."); let text = mat.text.to_string(); - let uppercased = key != lower_key; - return Event::Match((text, uppercased)); + return Event::Match(Selection { + text, + uppercased, + output_destination, + }); } else { // The prefix of a hint was entered, but we // still need more keys. @@ -525,11 +562,14 @@ impl<'a> Ui<'a> { Event::Exit } + // }}} + // Presenting {{{1 + /// Configure the terminal and display the `Ui`. /// /// - Setup steps: switch to alternate screen, switch to raw mode, hide the cursor. /// - Teardown steps: show cursor, back to main screen. - pub fn present(&mut self) -> Option<(String, bool)> { + pub fn present(&mut self) -> Option { use std::io::Write; use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; @@ -545,25 +585,35 @@ impl<'a> Ui<'a> { let selection = match self.listen(&mut stdin, &mut stdout) { Event::Exit => None, - Event::Match((text, uppercased)) => Some((text, uppercased)), + Event::Match(selection) => Some(selection), }; write!(stdout, "{}", cursor::Show).unwrap(); selection } + + // }}} } /// Compute each line's actual y offset if displayed in a terminal of width /// `term_width`. -fn get_line_offsets(lines: &Vec<&str>, term_width: u16) -> Vec { +fn get_line_offsets(lines: &[&str], term_width: u16) -> Vec { lines .iter() .scan(0, |offset, &line| { + // Save the value to return (yield is in unstable). let value = *offset; - // amount of extra y space taken by this line - let extra = line.trim_end().len() / term_width as usize; + let line_width = line.trim_end().chars().count() as isize; + + // Amount of extra y space taken by this line. + // If the line has n chars, on a term of width n, this does not + // produce an extra line; it needs to exceed the width by 1 char. + // In case the width is 0, we need to clamp line_width - 1 first. + let extra = cmp::max(0, line_width - 1) as usize / term_width as usize; + + // Update the offset of the next line. *offset = *offset + 1 + extra; Some(value) @@ -571,56 +621,18 @@ fn get_line_offsets(lines: &Vec<&str>, term_width: u16) -> Vec { .collect() } -/// Describes if, during rendering, a hint should aligned to the leading edge of -/// the matched text, or to its trailing edge. -#[derive(Debug, Clap)] -pub enum HintAlignment { - Leading, - Trailing, -} - -impl FromStr for HintAlignment { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match s { - "leading" => Ok(HintAlignment::Leading), - "trailing" => Ok(HintAlignment::Trailing), - _ => Err(ParseError::ExpectedString(String::from( - "leading or trailing", - ))), - } - } -} - -/// Describes the style of contrast to be used during rendering of the hint's -/// text. -/// -/// # Note -/// In practice, this is wrapped in an `Option`, so that the hint's text can be rendered with no style. -pub enum HintStyle { - /// The hint's text will be bold (leveraging `termion::style::Bold`). - Bold, - /// The hint's text will be italicized (leveraging `termion::style::Italic`). - Italic, - /// The hint's text will be underlined (leveraging `termion::style::Underline`). - Underline, - /// The hint's text will be surrounded by these chars. - Surround(char, char), -} - /// Returned value after the `Ui` has finished listening to events. enum Event { /// Exit with no selected matches, Exit, /// A vector of matched text and whether it was selected with uppercase. - Match((String, bool)), + Match(Selection), } #[cfg(test)] mod tests { use super::*; - use crate::alphabets; + use crate::textbuf::alphabet; #[test] fn test_render_all_lines() { @@ -645,7 +657,7 @@ path: /usr/local/bin/cargo"; }; let mut writer = vec![]; - Ui::render_base_text(&mut writer, &lines, &line_offsets, &colors); + ViewController::render_base_text(&mut writer, &lines, &line_offsets, &colors); let goto1 = cursor::Goto(1, 1); let goto2 = cursor::Goto(1, 2); @@ -682,7 +694,7 @@ path: /usr/local/bin/cargo"; hint_bg: Box::new(color::Cyan), }; - Ui::render_matched_text(&mut writer, text, focused, offset, &colors); + ViewController::render_matched_text(&mut writer, text, focused, offset, &colors); assert_eq!( writer, @@ -716,7 +728,7 @@ path: /usr/local/bin/cargo"; hint_bg: Box::new(color::Cyan), }; - Ui::render_matched_text(&mut writer, text, focused, offset, &colors); + ViewController::render_matched_text(&mut writer, text, focused, offset, &colors); assert_eq!( writer, @@ -752,7 +764,7 @@ path: /usr/local/bin/cargo"; let extra_offset = 0; let hint_style = None; - Ui::render_matched_hint( + ViewController::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), @@ -794,7 +806,7 @@ path: /usr/local/bin/cargo"; let extra_offset = 0; let hint_style = Some(HintStyle::Underline); - Ui::render_matched_hint( + ViewController::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), @@ -838,7 +850,7 @@ path: /usr/local/bin/cargo"; let extra_offset = 0; let hint_style = Some(HintStyle::Surround('{', '}')); - Ui::render_matched_hint( + ViewController::render_matched_hint( &mut writer, hint_text, (offset.0 + extra_offset, offset.1), @@ -866,14 +878,26 @@ path: /usr/local/bin/cargo"; #[test] /// Simulates rendering without any match. fn test_render_full_without_matches() { - let content = "lorem 127.0.0.1 lorem + let buffer = "lorem 127.0.0.1 lorem Barcelona https://en.wikipedia.org/wiki/Barcelona - "; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = false; let named_pat = vec![]; - let custom_regexes = vec![]; - let alphabet = alphabets::Alphabet("abcd".to_string()); - let mut model = model::Model::new(content, &alphabet, &named_pat, &custom_regexes, false); + let custom_patterns = vec![]; + let alphabet = alphabet::Alphabet("abcd".to_string()); + let reverse = false; + let unique_hint = false; + let mut model = textbuf::Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom_patterns, + reverse, + unique_hint, + ); let term_width: u16 = 80; let line_offsets = get_line_offsets(&model.lines, term_width); let rendering_colors = UiColors { @@ -889,14 +913,13 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let hint_alignment = HintAlignment::Leading; // create a Ui without any match - let ui = Ui { + let ui = ViewController { model: &mut model, term_width, line_offsets, - matches: vec![], // no matches - lookup_trie: SequenceTrie::new(), focus_index: 0, focus_wrap_around: false, + default_output_destination: OutputDestination::Tmux, rendering_colors: &rendering_colors, hint_alignment: &hint_alignment, hint_style: None, @@ -931,17 +954,28 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; #[test] /// Simulates rendering with matches. fn test_render_full_with_matches() { - let content = "lorem 127.0.0.1 lorem + let buffer = "lorem 127.0.0.1 lorem Barcelona https://en.wikipedia.org/wiki/Barcelona - "; + let lines = buffer.split('\n').collect::>(); + let use_all_patterns = true; let named_pat = vec![]; - let custom_regexes = vec![]; - let alphabet = alphabets::Alphabet("abcd".to_string()); + let custom_patterns = vec![]; + let alphabet = alphabet::Alphabet("abcd".to_string()); let reverse = true; - let mut model = model::Model::new(content, &alphabet, &named_pat, &custom_regexes, reverse); let unique_hint = false; + let mut model = textbuf::Model::new( + &lines, + &alphabet, + use_all_patterns, + &named_pat, + &custom_patterns, + reverse, + unique_hint, + ); let wrap_around = false; + let default_output_destination = OutputDestination::Tmux; let rendering_colors = UiColors { text_fg: Box::new(color::Black), @@ -956,10 +990,10 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; let hint_alignment = HintAlignment::Leading; let hint_style = None; - let ui = Ui::new( + let ui = ViewController::new( &mut model, - unique_hint, wrap_around, + default_output_destination, &rendering_colors, &hint_alignment, hint_style, @@ -1056,54 +1090,8 @@ Barcelona https://en.wikipedia.org/wiki/Barcelona - "; // .find(|(_idx, (&l, &r))| l != r); // println!("{:?}", diff_point); - assert_eq!(2, ui.matches.len()); + assert_eq!(2, ui.model.matches.len()); assert_eq!(writer, expected.as_bytes()); } } - -/// Holds color-related data, for clarity. -/// -/// - `focus_*` colors are used to render the currently focused matched text. -/// - `normal_*` colors are used to render other matched text. -/// - `hint_*` colors are used to render the hints. -#[derive(Clap, Debug)] -pub struct UiColors { - /// Foreground color for base text. - #[clap(long, default_value = "bright-cyan", parse(try_from_str = colors::parse_color))] - pub text_fg: Box, - - /// Background color for base text. - #[clap(long, default_value = "bright-white", parse(try_from_str = colors::parse_color))] - pub text_bg: Box, - - /// Foreground color for matches. - #[clap(long, default_value = "yellow", - parse(try_from_str = colors::parse_color))] - pub match_fg: Box, - - /// Background color for matches. - #[clap(long, default_value = "bright-white", - parse(try_from_str = colors::parse_color))] - pub match_bg: Box, - - /// Foreground color for the focused match. - #[clap(long, default_value = "magenta", - parse(try_from_str = colors::parse_color))] - pub focused_fg: Box, - - /// Background color for the focused match. - #[clap(long, default_value = "bright-white", - parse(try_from_str = colors::parse_color))] - pub focused_bg: Box, - - /// Foreground color for hints. - #[clap(long, default_value = "white", - parse(try_from_str = colors::parse_color))] - pub hint_fg: Box, - - /// Background color for hints. - #[clap(long, default_value = "magenta", - parse(try_from_str = colors::parse_color))] - pub hint_bg: Box, -} diff --git a/tmux-copyrat.tmux b/tmux-copyrat.tmux deleted file mode 100755 index a7b7c1f..0000000 --- a/tmux-copyrat.tmux +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -CURRENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -DEFAULT_COPYRAT_KEY="space" -COPYRAT_KEY=$(tmux show-option -gqv @copyrat-key) -COPYRAT_KEY=${COPYRAT_KEY:-$DEFAULT_COPYRAT_KEY} - -DEFAULT_COPYRAT_WINDOW_NAME="[copyrat]" -COPYRAT_WINDOW_NAME=$(tmux show-option -gqv @copyrat-window-name) -COPYRAT_WINDOW_NAME=${COPYRAT_WINDOW_NAME:-$DEFAULT_COPYRAT_WINDOW_NAME} - -BINARY="${CURRENT_DIR}/target/release/tmux-copyrat" - -tmux bind-key ${COPYRAT_KEY} new-window -d -n ${COPYRAT_WINDOW_NAME} "${BINARY} --window-name ${COPYRAT_WINDOW_NAME} --reverse --unique" - -if [ ! -f "$BINARY" ]; then - cd "${CURRENT_DIR}" && cargo build --release -fi