diff --git a/Cargo.lock b/Cargo.lock index a239993..cec21a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ansi-escapes" @@ -8,6 +8,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3c0daaaae24df5995734b689627f8fa02101bc5bbc768be3055b66a010d7af" +[[package]] +name = "ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b98e9c74265950a28ae39c02e290858714c23071ab4f71c61d2908e2edfe44" + [[package]] name = "ansi_term" version = "0.12.1" @@ -29,24 +35,26 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.1.0" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" -version = "1.0.73" +version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" dependencies = [ "jobserver", + "libc", + "shlex", ] [[package]] @@ -55,6 +63,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "2.34.0" @@ -63,19 +77,39 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.3.2", "strsim", "textwrap", - "unicode-width", + "unicode-width 0.1.14", "vec_map", ] +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + [[package]] name = "default-editor" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f287fe0eef3f4f016d56ac9a4ef3d03294dfbcc6765c4b5916079ad23985702" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + [[package]] name = "emoji-commit" version = "0.1.10" @@ -84,9 +118,14 @@ dependencies = [ "default-editor", "emoji-commit-type", "git2", - "log-update", + "input-string", + "log-update 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rustyline", + "rustyline-derive", "structopt", + "tempfile", "termion", + "tui-input", ] [[package]] @@ -95,11 +134,50 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e27ac934441d69b298c02aca5ee87a048b3d220d715557446ea12c61ccf5033" +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "error-code" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fd-lock" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -110,7 +188,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libgit2-sys", "log", @@ -137,42 +215,191 @@ dependencies = [ "libc", ] +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + [[package]] name = "idna" -version = "0.3.0" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "input-string" +version = "0.1.0" +dependencies = [ + "ansi-escapes 0.2.0", + "ansi_term", + "log-update 0.1.0", + "termion", + "unicode-width 0.2.0", ] [[package]] name = "jobserver" -version = "0.1.25" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.135" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libgit2-sys" -version = "0.14.0+1.5.0" +version = "0.14.2+1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" +checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" dependencies = [ "cc", "libc", @@ -182,6 +409,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall", +] + [[package]] name = "libssh2-sys" version = "0.2.23" @@ -198,9 +436,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" dependencies = [ "cc", "libc", @@ -208,13 +446,29 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "log-update" +version = "0.1.0" dependencies = [ - "cfg-if", + "ansi-escapes 0.1.1", ] [[package]] @@ -223,14 +477,47 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f04940d2443eee57bf0a6e9751a96c1d8af76ff02d8380b1fc0cc67acd393e5" dependencies = [ - "ansi-escapes", + "ansi-escapes 0.1.1", ] [[package]] -name = "numtoa" +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nibble_vec" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "numtoa" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl-probe" @@ -240,11 +527,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.76" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -253,15 +539,15 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "proc-macro-error" @@ -272,7 +558,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -289,40 +575,130 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.46" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.21" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] name = "redox_termios" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" +checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" + +[[package]] +name = "rustix" +version = "0.38.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "redox_syscall", + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", ] +[[package]] +name = "rustyline" +version = "15.0.0" +source = "git+https://github.com/kkawakam/rustyline.git?branch=master#5910f9e20b1ea6fdb2a7c1ea5a81e2ff391ff40a" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "rustyline-derive", + "unicode-segmentation", + "unicode-width 0.2.0", + "utf8parse", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustyline-derive" +version = "0.11.0" +source = "git+https://github.com/kkawakam/rustyline.git?branch=master#5910f9e20b1ea6fdb2a7c1ea5a81e2ff391ff40a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "serde" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.216" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.8.0" @@ -350,29 +726,64 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "syn" -version = "1.0.102" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "tempfile" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termion" -version = "1.5.6" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" +checksum = "7eaa98560e51a2cf4f0bb884d8b2098a9ea11ecf3b7078e9c68242c74cc923a7" dependencies = [ "libc", + "libredox", "numtoa", - "redox_syscall", "redox_termios", ] @@ -382,68 +793,82 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "unicode-width", + "unicode-width 0.1.14", ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] [[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "unicode-bidi" -version = "0.3.8" +name = "tui-input" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "e5d1733c47f1a217b7deff18730ff7ca4ecafc5771368f715ab072d679a36114" +dependencies = [ + "termion", + "unicode-width 0.2.0", +] [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] -name = "unicode-normalization" -version = "0.1.22" +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] -name = "unicode-segmentation" -version = "1.10.0" +name = "unicode-width" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "url" -version = "2.3.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" @@ -458,9 +883,9 @@ checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "winapi" @@ -483,3 +908,164 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.91", +] diff --git a/Cargo.toml b/Cargo.toml index 6350cfe..d1393c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,29 @@ readme = "readme.md" license = "MIT" [dependencies] -termion = "1.0" +termion = "4" log-update = "~0.1.0" default-editor = "~0.1.0" emoji-commit-type = "~0.1.1" git2 = "~0.15.0" structopt = "~0.3" ansi_term = "~0.12" +# tui-input = { version = "0.11.1", features = ["termion"], default-features = false } +rustyline = { git = "https://github.com/kkawakam/rustyline.git", branch = "master", features = [ + "derive", +] } +rustyline-derive = { git = "https://github.com/kkawakam/rustyline.git", branch = "master" } +tui-input = { version = "0.11.1", features = [ + "termion", +], default-features = false } +input-string = { path = "../input-string", features = [ + "termion", + "log-update", +], default-features = false } + + +[dev-dependencies] +tempfile = "~3.14.0" [profile.release] codegen-units = 1 diff --git a/src/crossterm_input.rs b/src/crossterm_input.rs new file mode 100644 index 0000000..85b8afc --- /dev/null +++ b/src/crossterm_input.rs @@ -0,0 +1,113 @@ +use std::io::{stdin, stdout, Result, Write}; +use termion::cursor::{Hide, Show}; +use termion::event::{Event, Key}; +use termion::input::{TermRead, TermReadEventsAndRaw}; +use termion::raw::IntoRawMode; +use termion::screen::IntoAlternateScreen; +use tui_input::backend::termion as backend; +use tui_input::backend::termion::EventHandler; +use tui_input::{Input, InputRequest, InputResponse}; + +use crate::commit_rules; +use ansi_term::Color::RGB; + +pub fn to_input_request(evt: &Event) -> Option { + use InputRequest::*; + match *evt { + + Event::Key(Key::Backspace) => Some(DeletePrevChar), + Event::Key(Key::Ctrl('h')) => Some(DeletePrevWord), + Event::Key(Key::Delete) => Some(DeleteNextChar), + Event::Key(Key::AltLeft) | Event::Key(Key::ShiftLeft) => Some(GoToPrevWord), + Event::Key(Key::AltRight) | Event::Key(Key::ShiftRight) => Some(GoToNextWord), + + Event::Key(Key::Left) | Event::Key(Key::Ctrl('b')) => Some(GoToPrevChar), + Event::Key(Key::Right) | Event::Key(Key::Ctrl('f')) => Some(GoToNextChar), + // Event::Key(Key::AltRight) => Some(GoToNextWord), + Event::Key(Key::Ctrl('u')) => Some(DeleteLine), + Event::Key(Key::Ctrl('w')) => Some(DeletePrevWord), + // Event::Key(Key::Ctrl(Key::Delete)) => Some(DeleteNextWord), + Event::Key(Key::Ctrl('a')) | Event::Key(Key::Home) => Some(GoToStart), + Event::Key(Key::Ctrl('e')) | Event::Key(Key::End) => Some(GoToEnd), + Event::Key(Key::Char('\t')) => None, + Event::Key(Key::Char(c)) => Some(InsertChar(c)), + _ => None, + } +} + +#[derive(Default)] +struct CrosstermInput(Input); + +impl From<&str> for CrosstermInput { + fn from(s: &str) -> Self { + CrosstermInput(Input::new(s.to_string())) + } +} + +impl std::fmt::Display for CrosstermInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl CrosstermInput { + fn value(&self) -> &str { + &self.0.value() + } + + fn cursor(&self) -> usize { + self.0.cursor() + } +} + +impl EventHandler for CrosstermInput { + fn handle_event(&mut self, evt: &Event) -> InputResponse { + let request = to_input_request(evt)?; + self.0.handle(request) + } +} + +fn format_message(selected_emoji: &'static str, input: &str) -> String { + let rule_text = commit_rules::check_message(input) + .map(|result| format!("{}", result)) + .collect::>() + .join("\r\n"); + let text = format!( + "\r\nRemember the seven rules of a great Git commit message:\r\n\r\n{}\r\n\r\n{}\r\n{}", + rule_text, + RGB(105, 105, 105).paint("Enter - finish, Ctrl-C - abort, Ctrl-E - continue editing in $EDITOR"), + selected_emoji, + ); + + return text; +} + +pub fn crossterm_input() -> Result<()> { + let mut input: CrosstermInput = "Hello ".into(); + { + let stdin = stdin(); + let mut stdout = stdout().into_raw_mode()?;//.into_alternate_screen()?; + + // write!(&mut stdout, "{}", Hide)?; + backend::write(&mut stdout, input.value(), input.cursor(), (0, 0), 15)?; + stdout.flush()?; + + for events_and_raw in stdin.events_and_raw() { + let (evt, raw) = events_and_raw?; + println!("evt: {:?}, raw: {:?}", evt, raw); + if evt == Event::Key(Key::Esc) || evt == Event::Key(Key::Char('\n')) { + break; + } + + if input.handle_event(&evt).is_some() { + backend::write(&mut stdout, input.value(), input.cursor(), (0, 0), 15)?; + stdout.flush()?; + } + } + + write!(stdout, "{}", Show)?; + } + + println!("{}", input); + Ok(()) +} \ No newline at end of file diff --git a/src/input_string.rs b/src/input_string.rs new file mode 100644 index 0000000..f1366c5 --- /dev/null +++ b/src/input_string.rs @@ -0,0 +1,249 @@ +use ansi_term::Colour::White; + +pub struct InputString { + value: String, + position: usize, + is_control_input: bool, +} + +impl Default for InputString { + fn default() -> Self { + Self { + value: String::new(), + position: 0, + is_control_input: false, + } + } +} + +impl InputString { + pub fn new(initial_value: Option) -> Self { + Self { + value: initial_value.unwrap_or_default(), + ..Default::default() + } + } + + pub fn as_str(&self) -> &str { + self.value.as_str() + } + + pub fn trim(&self) -> &str { + self.value.trim() + } + pub fn push(&mut self, c: char) { + if self.is_control_input { + self.handle_control(c); + return; + } + if self.position == self.value.len() { + self.value.push(c); + } else { + let mut new_string = String::new(); + new_string.push_str(&self.value.as_str()[0..self.position]); + new_string.push(c); + new_string.push_str(&self.value.as_str()[self.position..]); + self.value = new_string; + } + self.position += 1; + } + + pub fn delete(&mut self) { + if self.position == self.value.len() { + return; + } + if self.position == 0 { + self.value.remove(0); + } else { + let mut new_string = String::new(); + new_string.push_str(&self.value.as_str()[0..self.position]); + new_string.push_str(&self.value.as_str()[self.position + 1..]); + self.value = new_string; + } + } + + pub fn go_to_start(&mut self) { + self.position = 0; + } + + pub fn go_to_end(&mut self) { + self.position = self.value.len(); + } + + pub fn delete_word_left(&mut self) { + if self.position == 0 { + return; + } + + let mut pos = self.position; + let mut found_word = false; + + while pos > 0 { + pos -= 1; + let c = self.value.chars().nth(pos).unwrap(); + if c.is_whitespace() { + if found_word { + pos += 1; + break; + } + } else { + found_word = true; + } + } + + let mut new_string = String::new(); + new_string.push_str(&self.value.as_str()[0..pos]); + new_string.push_str(&self.value.as_str()[self.position..]); + self.value = new_string; + self.position = pos; + } + + pub fn delete_word_right(&mut self) { + if self.position >= self.value.len() { + return; + } + + let mut pos = self.position; + let mut found_word = false; + + while pos < self.value.len() { + let c = self.value.chars().nth(pos).unwrap(); + if c.is_whitespace() { + if found_word { + break; + } + } else { + found_word = true; + } + pos += 1; + } + + let mut new_string = String::new(); + new_string.push_str(&self.value.as_str()[0..self.position]); + new_string.push_str(&self.value.as_str()[pos..]); + self.value = new_string; + } + + pub fn backspace(&mut self) { + if self.position == 0 { + return; + } + if self.position == self.value.len() { + self.value.pop(); + } else { + let mut new_string = String::new(); + new_string.push_str(&self.value.as_str()[0..self.position - 1]); + new_string.push_str(&self.value.as_str()[self.position..]); + self.value = new_string; + } + self.position -= 1; + } + + pub fn go_char_left(&mut self) { + if self.position > 1 { + self.position -= 1; + } else { + self.position = 0; + } + } + pub fn go_char_right(&mut self) { + if self.position < self.value.len() { + self.position += 1; + } else { + self.position = self.value.len(); + } + } + pub fn handle_control(&mut self, c: char) { + if !self.is_control_input && c.is_control() { + self.is_control_input = true + } + if self.is_control_input { + match c { + 'D' => self.go_word_left(), + 'C' => self.go_word_right(), + _ => { + return; + } + } + self.is_control_input = false + } + + match c { + 'b' => self.go_word_left(), + 'f' => self.go_word_right(), + _ => {} + } + } + pub fn go_word_left(&mut self) { + if self.position == 0 { + return; + } + let mut new_position = self.position; + while new_position > 0 + && self + .value + .as_str() + .chars() + .nth(new_position - 1) + .unwrap() + .is_whitespace() + { + new_position -= 1; + } + while new_position > 0 + && !self + .value + .as_str() + .chars() + .nth(new_position - 1) + .unwrap() + .is_whitespace() + { + new_position -= 1; + } + self.position = new_position; + } + pub fn go_word_right(&mut self) { + if self.position == self.value.len() { + return; + } + let mut new_position = self.position; + while new_position < self.value.len() + && self + .value + .as_str() + .chars() + .nth(new_position) + .unwrap() + .is_whitespace() + { + new_position += 1; + } + while new_position < self.value.len() + && !self + .value + .as_str() + .chars() + .nth(new_position) + .unwrap() + .is_whitespace() + { + new_position += 1; + } + self.position = new_position; + } + pub fn format(&self) -> String { + if self.position == self.value.len() { + format!("{}{}", &self.value.as_str(), White.underline().paint(" ")) + } else { + format!( + "{}{}{}", + &self.value.as_str()[0..self.position], + White + .underline() + .paint(&self.value.as_str()[self.position..self.position + 1]), + &self.value.as_str()[self.position + 1..] + ) + } + } +} diff --git a/src/input_string_input.rs b/src/input_string_input.rs new file mode 100644 index 0000000..6c36970 --- /dev/null +++ b/src/input_string_input.rs @@ -0,0 +1,73 @@ +use ansi_term::Colour::{Green, Red, RGB}; +use std::io::{stderr, stdin, stdout, Result, Write}; +use termion::cursor::{Hide, Show}; +use termion::event::{Event, Key}; +use termion::input::TermRead; +use termion::raw::IntoRawMode; + +use input_string::backend::termion::EventHandler; +use input_string::Input; +use log_update::LogUpdate; + +fn get_text(input: &Input, rule_text: &str, selected_emoji: &str) -> String { + format!( + "\r\nRemember the seven rules of a great Git commit message:\r\n\r\n{}\r\n\r\n{}\r\n{} {}", + rule_text, + RGB(105, 105, 105) + .paint("Enter - finish, Ctrl-C - abort, Ctrl-E - continue editing in $EDITOR"), + selected_emoji, + input_string::backend::log_update::format_input(input) + ) +} + +pub fn input_string_input() -> Result<()> { + let mut log_update = LogUpdate::new(stderr()).unwrap(); + let mut raw_output = stderr().into_raw_mode().unwrap(); + let rule_text = "1. Separate subject from body with a blank line"; + let selected_emoji = "👍"; + + let mut aborted = false; + + let mut input: Input = "Hello ".into(); + + { + let stdin = stdin(); + + log_update + .render(&get_text(&input, &rule_text, &selected_emoji)) + .unwrap(); + + for evt in stdin.events() { + let evt = evt?; + if evt == Event::Key(Key::Esc) || evt == Event::Key(Key::Ctrl('c')) { + aborted = true; + break; + } + if evt == Event::Key(Key::Char('\n')) { + break; + } + + if input.handle_event(&evt).is_some() { + let text = get_text(&input, &rule_text, &selected_emoji); + + log_update.render(&text).unwrap(); + } + + if aborted { + break; + } + } + } + + log_update.clear().unwrap(); + raw_output.flush().unwrap(); + let value = input.value(); + + if aborted { + print!("Aborted"); + } else { + print!("Finished: {}", value); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index e6fce13..b6b5835 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,54 +2,77 @@ use std::env; use std::error::Error; use std::fmt; use std::fs::File; -use std::io::{BufRead, BufReader, Seek, Write, stderr, stdin}; -use std::process::{Command, exit}; +use std::io::{stderr, stdin, Seek, Write}; use std::path::PathBuf; +use std::process::{exit, Command}; use std::str::FromStr; +use input_string_input::input_string_input; use termion::event::Key; use termion::input::TermRead; use termion::raw::IntoRawMode; +use ansi_term::Colour::{Green, Red, RGB}; use emoji_commit_type::CommitType; use log_update::LogUpdate; +use message::{git_message_is_empty, git_parse_existing_message}; use structopt::StructOpt; -use ansi_term::Colour::{RGB, Green, Red, White}; mod commit_rules; +mod crossterm_input; mod git; +mod input_string_input; +mod message; + +mod input_string; +mod rusty_input; +use crate::input_string::InputString; +use crate::rusty_input::rusty_main; impl fmt::Display for commit_rules::CommitRuleValidationResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", if self.pass { - Green.paint("✔") - } else { - Red.paint("✖") - }, self.description) + write!( + f, + "{} {}", + if self.pass { + Green.paint("✔") + } else { + Red.paint("✖") + }, + self.description + ) } } fn print_emoji_selector(log_update: &mut LogUpdate, selected: &CommitType) { let text = CommitType::iter_variants() - .map(|t| format!("{} {} \u{001b}[90m{:<5}\u{001b}[39m {}", if t == *selected { "👉" } else { " " }, t.emoji(), t.bump_level().name().to_lowercase(), t.description())) + .map(|t| { + format!( + "{} {} \u{001b}[90m{:<5}\u{001b}[39m {}", + if t == *selected { "👉" } else { " " }, + t.emoji(), + t.bump_level().name().to_lowercase(), + t.description() + ) + }) .collect::>() .join("\r\n"); log_update.render(&text).unwrap(); } -fn commit_type_at_index (index: u8) -> Option { +fn commit_type_at_index(index: u8) -> Option { CommitType::iter_variants().nth(index as usize) } -fn select_emoji() -> Option<&'static str> { +fn select_emoji(initial_selected: Option) -> Option<&'static str> { let mut log_update = LogUpdate::new(stderr()).unwrap(); let mut raw_output = stderr().into_raw_mode().unwrap(); let mut key_stream = stdin().keys(); let mut aborted = false; - let mut selected = CommitType::Breaking; + let mut selected = initial_selected.unwrap_or(CommitType::Breaking); // Clear possibly printed "hint" from git log_update.render("").unwrap(); @@ -58,60 +81,122 @@ fn select_emoji() -> Option<&'static str> { print_emoji_selector(&mut log_update, &selected); match key_stream.next().unwrap().unwrap() { - Key::Ctrl('c') => { aborted = true; break }, + Key::Ctrl('c') | Key::Esc => { + aborted = true; + break; + } Key::Char('\n') => break, - Key::Up | Key::Char('k') | Key::Char('K') => selected = selected.prev_variant().unwrap_or_else(CommitType::last_variant), - Key::Down | Key::Char('j') | Key::Char('J') => selected = selected.next_variant().unwrap_or_else(CommitType::first_variant), - Key::Char(key @ '1' ..= '9') => { if let Some(t) = commit_type_at_index((key as u8) - b'1') { selected = t; } }, - _ => {}, + Key::Up | Key::Char('k') | Key::Char('K') => { + selected = selected + .prev_variant() + .unwrap_or_else(CommitType::last_variant) + } + Key::Down | Key::Char('j') | Key::Char('J') => { + selected = selected + .next_variant() + .unwrap_or_else(CommitType::first_variant) + } + Key::Char(key @ '1'..='9') => { + if let Some(t) = commit_type_at_index((key as u8) - b'1') { + selected = t; + } + } + _ => {} } } log_update.clear().unwrap(); raw_output.flush().unwrap(); - if aborted { None } else { Some(selected.emoji()) } + if aborted { + None + } else { + Some(selected.emoji()) + } } -fn collect_commit_message(selected_emoji: &'static str, launch_editor: &mut bool) -> Option { +fn collect_commit_message_rusty( + selected_emoji: &'static str, + initial_message: Option, + launch_editor: &mut bool, +) -> Option { + // let mut log_update = LogUpdate::new(stderr()).unwrap(); + // let mut raw_output = stderr().into_raw_mode().unwrap(); + + // let mut key_stream = stdin().keys(); + + // let mut aborted = false; + + let _ = rusty_main(initial_message.unwrap_or("".to_string()).as_str()); + + None +} + +fn collect_commit_message( + selected_emoji: &'static str, + initial_message: Option, + launch_editor: &mut bool, +) -> Option { let mut log_update = LogUpdate::new(stderr()).unwrap(); let mut raw_output = stderr().into_raw_mode().unwrap(); let mut key_stream = stdin().keys(); let mut aborted = false; - let mut input = String::new(); + let mut input = InputString::new(initial_message); loop { - let rule_text = commit_rules::check_message(&input) + let rule_text = commit_rules::check_message(&input.as_str()) .map(|result| format!("{}", result)) .collect::>() .join("\r\n"); let text = format!( - "\r\nRemember the seven rules of a great Git commit message:\r\n\r\n{}\r\n\r\n{}\r\n{} {}{}", + "\r\nRemember the seven rules of a great Git commit message:\r\n\r\n{}\r\n\r\n{}\r\n{} {}", rule_text, RGB(105, 105, 105).paint("Enter - finish, Ctrl-C - abort, Ctrl-E - continue editing in $EDITOR"), selected_emoji, - input, - White.underline().paint(" ") + input.format() ); log_update.render(&text).unwrap(); match key_stream.next().unwrap().unwrap() { - Key::Ctrl('c') => { aborted = true; break }, + Key::Ctrl('c') | Key::Esc => { + aborted = true; + break; + } Key::Char('\n') => break, + Key::CtrlLeft | Key::AltLeft | Key::ShiftLeft => input.go_word_left(), + Key::CtrlRight | Key::AltRight | Key::ShiftRight => input.go_word_right(), + Key::Alt('\u{7f}') => input.delete_word_left(), + Key::Alt(c) => input.handle_control(c), Key::Char(c) => input.push(c), - Key::Backspace => { input.pop(); }, - Key::Ctrl('e') => { *launch_editor = true; break }, - _ => {}, + Key::Backspace => input.backspace(), + Key::Delete => input.delete(), + Key::Left => input.go_char_left(), + Key::Right => input.go_char_right(), + Key::Home => input.go_to_start(), + Key::End => input.go_to_end(), + Key::Ctrl('h') => input.delete_word_left(), + Key::Ctrl('l') => input.delete_word_right(), + Key::Ctrl('e') => { + *launch_editor = true; + break; + } + other => { + println!("other: {:?}\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", other); + } } } log_update.clear().unwrap(); raw_output.flush().unwrap(); - if aborted { None } else { Some(String::from(input.trim())) } + if aborted { + None + } else { + Some(String::from(input.trim())) + } } fn abort() -> ! { @@ -140,36 +225,43 @@ fn launch_default_editor(out_path: PathBuf) { fn launch_git_with_self_as_editor() { let self_path = std::env::current_exe().unwrap(); - run_cmd(Command::new("git").arg("commit").env("GIT_EDITOR", self_path)) -} - -fn git_message_is_empty(file: &mut File) -> bool { - for line in BufReader::new(file).lines() { - let line = line.expect("Failed to read line from git message file"); - - if !line.starts_with('#') && !line.is_empty() { - return false; - } - } - true + run_cmd( + Command::new("git") + .arg("commit") + .env("GIT_EDITOR", self_path), + ) } fn collect_information_and_write_to_file(out_path: PathBuf) { - let mut file = File::options().read(true).write(true).create(true).open(&out_path).unwrap(); + let mut file = File::options() + .read(true) + .write(true) + .create(true) + .open(&out_path) + .unwrap(); + let mut initial_message: Option = None; + let mut initial_commit_type: Option = None; if !git_message_is_empty(&mut file) { - launch_default_editor(out_path); - return; + file.rewind().unwrap(); + if let Some((commit_type, message)) = git_parse_existing_message(&mut file) { + initial_commit_type = Some(commit_type); + initial_message = Some(message); + } else { + launch_default_editor(out_path); + return; + } } - let maybe_emoji = select_emoji(); + let maybe_emoji = select_emoji(initial_commit_type); if maybe_emoji == None { abort(); } if let Some(emoji) = maybe_emoji { let mut launch_editor = false; - let maybe_message = collect_commit_message(emoji, &mut launch_editor); + let maybe_message = + collect_commit_message_rusty(emoji, initial_message, &mut launch_editor); if maybe_message == None { abort(); } @@ -208,7 +300,11 @@ fn validate(refspecs: Vec) -> Result<(), Box> { for message in messages { let validation_errors = commit_rules::check_message_with_emoji(&message) .filter_map(|result| { - if !result.pass { Some(format!("\t{}", result)) } else { None } + if !result.pass { + Some(format!("\t{}", result)) + } else { + None + } }) .collect::>(); if !validation_errors.is_empty() { @@ -216,13 +312,16 @@ fn validate(refspecs: Vec) -> Result<(), Box> { let validation_result_text = validation_errors.join("\r\n"); let text = format!( "\r\nCommit:\r\n\t{}\r\nValidation Errors:\r\n{}\r\n", - message, - validation_result_text, + message, validation_result_text, ); println!("{}", text); } } - if has_errors {Err(Box::new(ValidationError{}))} else { Ok(()) } + if has_errors { + Err(Box::new(ValidationError {})) + } else { + Ok(()) + } } #[derive(Debug)] @@ -257,35 +356,51 @@ struct Opt { #[structopt( help = "Path passed by Git where commit message should be written to. Path will be passed through to $EDITOR if doing a interactive rebase.", group = "mutually-exclusive", - hidden = true, + hidden = true )] out_path: Option, #[structopt( long = "validate", help = "Validate the provided refspecs against the commit rules. Refspecs can be passed as is or in the special notation defined in git-rev-list.", - group = "mutually-exclusive", + group = "mutually-exclusive" )] refspecs: Option>, } fn main() -> Result<(), Box> { match Opt::from_args() { - Opt {out_path: None, refspecs: Some(refspecs)} => validate(refspecs), - Opt {out_path: Some(OutPath::EditMessage(out_path)), refspecs: None} => { + Opt { + out_path: None, + refspecs: Some(refspecs), + } => validate(refspecs), + Opt { + out_path: Some(OutPath::EditMessage(out_path)), + refspecs: None, + } => { collect_information_and_write_to_file(out_path); Ok(()) - }, + } Opt { - out_path: Some(OutPath::RebaseTodo(out_path) | OutPath::AddPHunkEdit(out_path) | OutPath::MergeMessage(out_path)), + out_path: + Some( + OutPath::RebaseTodo(out_path) + | OutPath::AddPHunkEdit(out_path) + | OutPath::MergeMessage(out_path), + ), refspecs: None, } => { launch_default_editor(out_path); Ok(()) - }, + } _ => { launch_git_with_self_as_editor(); Ok(()) } } } + +// fn main() -> Result<(), Box> { +// rusty_main()?; +// Ok(()) +// } diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 0000000..35bd740 --- /dev/null +++ b/src/message.rs @@ -0,0 +1,90 @@ +use emoji_commit_type::CommitType; +use std::{ + fs::File, + io::{BufRead, BufReader, Read}, +}; +use termion::input::TermRead; + +pub fn git_parse_existing_message(reader: &mut R) -> Option<(CommitType, String)> { + let first_line = reader.read_line().unwrap().unwrap(); + + if first_line.is_empty() { + return None; + } + + let first_str = first_line.chars().next().unwrap().to_string(); + + let commit_type = + CommitType::iter_variants().find(|commit_type| first_str == commit_type.emoji()); + + if commit_type == None { + return None; + } + + // Check that the rest of the commit message is empty (i.e. no body) + if !git_message_is_empty(reader) { + return None; + } + + let emoji = commit_type.unwrap().emoji().to_string(); + let message = first_line.replace(&emoji, "").trim().to_string(); + Some((commit_type.unwrap(), message)) +} + +pub(crate) fn git_message_is_empty(reader: R) -> bool { + for line in BufReader::new(reader).lines() { + let line = line.expect("Failed to read line from git message file"); + + if !line.starts_with('#') && !line.is_empty() { + return false; + } + } + true +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{Seek, Write}; + use tempfile::NamedTempFile; + + #[test] + fn test_git_message_is_empty() { + let test_cases: Vec<(&str, bool)> = vec![ + ("# Comment line\n\n# Another comment", true), + ("yo", false), + ("# Comment\nActual content", false), + ("", true), + ]; + + for (input, expected) in test_cases { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "{}", input).unwrap(); + file.flush().unwrap(); + file.rewind().unwrap(); + assert_eq!(git_message_is_empty(&mut file), expected); + } + } + + #[test] + fn test_git_parse_existing_message() { + let test_cases: Vec<(&str, Option<(CommitType, &str)>)> = vec![ + ("🎉 Added a new feature", Some((CommitType::Feature, "Added a new feature"))), + ("Invalid message", None), + ("🐛 Fix critical bug", Some((CommitType::Bugfix, "Fix critical bug"))), + ("🎉 Added a new feature\n\n# Comment", Some((CommitType::Feature, "Added a new feature"))), + ("💥", Some((CommitType::Breaking, ""))), + ]; + + for (input, expected) in test_cases { + let mut file = NamedTempFile::new().unwrap(); + writeln!(file, "{}", input).unwrap(); + file.flush().unwrap(); + file.rewind().unwrap(); + assert_eq!( + git_parse_existing_message(&mut file), + expected.map(|(t, m)| (t, m.to_string())) + ); + } + } +} diff --git a/src/rusty_input.rs b/src/rusty_input.rs new file mode 100644 index 0000000..399101d --- /dev/null +++ b/src/rusty_input.rs @@ -0,0 +1,274 @@ +use rustyline::error::ReadlineError; +use rustyline::{ + Cmd, ConditionalEventHandler, Config, DefaultEditor, Event, EventContext, EventHandler, + ExternalPrinter, Hinter, KeyCode, KeyEvent, Modifiers, Movement, RepeatCount, Result, Word, +}; + +use std::borrow::Cow::{self, Borrowed, Owned}; + +use std::collections::HashSet; +use std::thread; +use std::time::Duration; + +use rustyline::highlight::Highlighter; +use rustyline::hint::{Hint, Hinter, HistoryHinter}; +use rustyline::history::DefaultHistory; +use rustyline::Context; +use rustyline::Editor; +use rustyline::{Completer, Helper, Validator}; + +#[derive(Completer, Helper, Validator, rustyline::Highlighter)] +struct DIYHinter { + // It's simple example of rustyline, for more efficient, please use ** radix trie ** + hints: HashSet, +} + +#[derive(Hash, Debug, PartialEq, Eq)] +struct CommandHint { + display: String, + complete_up_to: usize, +} + +impl Hint for CommandHint { + fn display(&self) -> &str { + &self.display + } + + fn completion(&self) -> Option<&str> { + if self.complete_up_to > 0 { + Some(&self.display[..self.complete_up_to]) + } else { + None + } + } +} + +impl CommandHint { + fn new(text: &str, complete_up_to: &str) -> Self { + assert!(text.starts_with(complete_up_to)); + Self { + display: text.into(), + complete_up_to: complete_up_to.len(), + } + } + + fn suffix(&self, strip_chars: usize) -> Self { + Self { + display: self.display[strip_chars..].to_owned(), + complete_up_to: self.complete_up_to.saturating_sub(strip_chars), + } + } +} + +impl Hinter for DIYHinter { + type Hint = CommandHint; + + fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option { + if line.is_empty() || pos < line.len() { + return None; + } + + self.hints + .iter() + .filter_map(|hint| { + // expect hint after word complete, like redis cli, add condition: + // line.ends_with(" ") + if hint.display.starts_with(line) { + Some(hint.suffix(pos)) + } else { + None + } + }) + .next() + } +} + +fn diy_hints() -> HashSet { + let mut set = HashSet::new(); + set.insert(CommandHint::new("help", "help")); + set.insert(CommandHint::new("get key", "get ")); + set.insert(CommandHint::new("set key value", "set ")); + set.insert(CommandHint::new("hget key field", "hget ")); + set.insert(CommandHint::new("hset key field value", "hset ")); + set +} + +struct FilteringEventHandler; +impl ConditionalEventHandler for FilteringEventHandler { + fn handle(&self, evt: &Event, _: RepeatCount, _: bool, _: &EventContext) -> Option { + // if let Event::KeySeq(key_seq) = evt { + // match (key_seq.get(0), key_seq.get(1)) { + // (Some(KeyEvent(KeyCode::Backspace, Modifiers::NONE)), Some(KeyEvent(KeyCode::Delete, Modifiers::NONE))) => { + // return Some(Cmd::Kill(Movement::BackwardWord(1, Word::Big))); + // } + // (Some(KeyEvent(KeyCode::Delete, Modifiers::NONE)), Some(KeyEvent(KeyCode::Backspace, Modifiers::NONE))) => { + // return Some(Cmd::Kill(Movement::ForwardWord(1, rustyline::At::AfterEnd, Word::Big))); + // } + + // _ => { + // // + // } + // } + // } + // if let Event::KeySeq(key_seq) = evt { + // if key_seq == &[KeyEvent(KeyCode::Backspace, Modifiers::CTRL)] { + // return Some(Cmd::Kill(Movement::BackwardWord(1, Word::Big))); + // } else if key_seq == &[KeyEvent(KeyCode::Delete, Modifiers::CTRL)] { + // return Some(Cmd::Kill(Movement::ForwardWord(1, rustyline::At::AfterEnd, Word::Big))); + // } + // } + println!("evt: {:?}", evt); + if let Some(KeyEvent(c, m)) = evt.get(0) { + match (c, m) { + (KeyCode::Backspace, &Modifiers::CTRL) => { + Some(Cmd::Kill(Movement::BackwardWord(1, Word::Big))) + } + (KeyCode::Delete, &Modifiers::CTRL) => Some(Cmd::Kill(Movement::ForwardWord( + 1, + rustyline::At::AfterEnd, + Word::Big, + ))), + // (KeyCode::Esc, _) => Some(Cmd::Noop), + _ => None, + } + } else { + None + } + + // if let Some(KeyEvent(KeyCode::Char(c), m)) = evt.get(0) { + // if m.contains(Modifiers::CTRL) || m.contains(Modifiers::ALT) || c.is_ascii_digit() { + // None + // } else { + // None + // } + // } else { + // None + // } + } +} + +#[derive(Completer, Helper, Hinter, Validator)] +struct MyHelper(#[rustyline(Hinter)] HistoryHinter); + +impl Highlighter for MyHelper { + fn highlight_prompt<'b, 's: 'b, 'p: 'b>( + &'s self, + prompt: &'p str, + default: bool, + ) -> Cow<'b, str> { + if default { + Owned(format!("\x1b[1;31mpp{prompt}\x1b[m")) + } else { + Owned(format!("\x1b[1;31m{prompt}\x1b[m")) + } + } + + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + Owned(format!("\x1b[1m{hint}\x1b[m")) + } +} + +pub fn rusty_main(initial_text: &str) -> Result<()> { + let config = Config::builder() + .behavior(rustyline::config::Behavior::PreferTerm) + .keyseq_timeout(Some(30)) + .build(); + + // let mut rl = DefaultEditor::with_config(config)?; + + // let h = DIYHinter { hints: diy_hints() }; + + let mut rl: Editor = Editor::with_config(config)?; + rl.set_helper(Some(MyHelper(HistoryHinter::new()))); + + // rl.set_helper(Some(h)); + + let mut printer = rl + .create_external_printer() + .expect("Failed to create external printer"); + + // thread::spawn(move || { + // let mut i = 0usize; + // loop { + // printer + // .print(format!("External message #{i}")) + // .expect("External print failure"); + // let wait_ms = 1000; + // thread::sleep(Duration::from_millis(wait_ms)); + // i += 1; + // } + // }); + + // rl.bind_sequence( + // Event::Any, + // EventHandler::Conditional(Box::new(FilteringEventHandler)), + // ); + + rl.bind_sequence( + KeyEvent(KeyCode::Left, Modifiers::SHIFT), + EventHandler::Simple(Cmd::Move(Movement::BackwardWord(1, Word::Big))), + ); + rl.bind_sequence( + KeyEvent(KeyCode::Right, Modifiers::SHIFT), + EventHandler::Simple(Cmd::Move(Movement::ForwardWord( + 1, + rustyline::At::AfterEnd, + Word::Big, + ))), + ); + rl.bind_sequence( + KeyEvent(KeyCode::Backspace, Modifiers::CTRL), + EventHandler::Simple(Cmd::Kill(Movement::BackwardWord(1, Word::Big))), + ); + rl.bind_sequence( + KeyEvent(KeyCode::Delete, Modifiers::CTRL), + EventHandler::Simple(Cmd::Kill(Movement::ForwardWord( + 1, + rustyline::At::AfterEnd, + Word::Big, + ))), + ); + + rl.bind_sequence( + KeyEvent(KeyCode::Esc, Modifiers::empty()), + EventHandler::Simple(Cmd::Interrupt), + ); + + loop { + let readline = rl.readline(">> "); + match readline { + Ok(line) => { + _ = rl.add_history_entry(line.as_str()); + println!("Line: {}", line); + } + Err(ReadlineError::Interrupted) => { + println!("CTRL-C"); + break; + } + Err(ReadlineError::Eof) => { + println!("CTRL-D"); + break; + } + Err(err) => { + println!("Error: {:?}", err); + break; + } + } + } + + Ok(()) +} + +pub struct InputStringRL { + value: String, + position: usize, +} + +impl InputStringRL { + pub fn new(initial_text: &str) -> Self { + Self { + value: initial_text.to_string(), + position: 0, + } + } +}