diff options
42 files changed, 6932 insertions, 1407 deletions
@@ -3,10 +3,25 @@ version = 3 [[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] name = "anyhow" -version = "1.0.45" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" +checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd" [[package]] name = "arrayvec" @@ -15,297 +30,498 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] -name = "async-channel" -version = "1.6.1" +name = "async-stream" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625" dependencies = [ - "concurrent-queue", - "event-listener", + "async-stream-impl", "futures-core", ] [[package]] -name = "async-executor" -version = "1.4.1" +name = "async-stream-impl" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308" dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "once_cell", - "slab", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "async-global-executor" -version = "2.0.2" +name = "async-trait" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" dependencies = [ - "async-channel", - "async-executor", - "async-io", - "async-mutex", - "blocking", - "futures-lite", - "num_cpus", - "once_cell", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "async-io" -version = "1.6.0" +name = "atty" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "concurrent-queue", - "futures-lite", + "hermit-abi", "libc", - "log", - "once_cell", - "parking", - "polling", - "slab", - "socket2", - "waker-fn", "winapi", ] [[package]] -name = "async-lock" -version = "2.4.0" +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" dependencies = [ - "event-listener", + "serde", ] [[package]] -name = "async-mutex" -version = "1.4.0" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ - "event-listener", + "block-padding", + "byte-tools", + "byteorder", + "generic-array", ] [[package]] -name = "async-process" -version = "1.3.0" +name = "block-padding" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" dependencies = [ - "async-io", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "libc", - "once_cell", - "signal-hook", - "winapi", + "byte-tools", ] [[package]] -name = "async-std" -version = "1.10.0" +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8056f1455169ab86dd47b47391e4ab0cbd25410a70e9fe675544f49bafaf952" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" dependencies = [ - "async-channel", - "async-global-executor", - "async-io", - "async-lock", - "async-process", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "num_cpus", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", + "jobserver", ] [[package]] -name = "async-task" -version = "4.0.3" +name = "cfg-if" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "atomic-waker" -version = "1.0.0" +name = "clap" +version = "3.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" +checksum = "ced1892c55c910c1219e98d6fc8d71f6bddba7905866ce740066d8bfea859312" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "terminal_size", + "textwrap", +] [[package]] -name = "autocfg" -version = "1.0.1" +name = "clap_derive" +version = "3.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "bitflags" -version = "1.3.2" +name = "console-api" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "cc347c19eb5b940f396ac155822caee6662f850d97306890ac3773ed76c90c5a" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-build", + "tracing-core", +] [[package]] -name = "blocking" -version = "1.1.0" +name = "console-subscriber" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046e47d4b2d391b1f6f8b407b1deb8dee56c1852ccd868becf2710f601b5f427" +checksum = "565a7dfea2d10dd0e5c57cc394d5d441b1910960d8c9211ed14135e0e6ec3a20" dependencies = [ - "async-channel", - "async-task", - "atomic-waker", - "fastrand", - "futures-lite", - "once_cell", + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", ] [[package]] -name = "bumpalo" -version = "3.8.0" +name = "crc32fast" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] [[package]] -name = "cache-padded" -version = "1.1.1" +name = "crossbeam-channel" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" +checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] [[package]] -name = "cc" -version = "1.0.72" +name = "crossbeam-utils" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" +dependencies = [ + "cfg-if", + "lazy_static", +] [[package]] -name = "cfg-if" -version = "1.0.0" +name = "digest" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array", +] [[package]] -name = "chrono" -version = "0.4.19" +name = "directories" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" dependencies = [ "libc", - "num-integer", - "num-traits", - "time", + "redox_users", "winapi", ] [[package]] -name = "concurrent-queue" -version = "1.2.2" +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fastrand" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" dependencies = [ - "cache-padded", + "instant", ] [[package]] -name = "crossbeam-utils" -version = "0.8.5" +name = "filetime" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" dependencies = [ "cfg-if", - "lazy_static", + "libc", + "redox_syscall", + "winapi", ] [[package]] -name = "ctor" -version = "0.1.21" +name = "fixedbitset" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" + +[[package]] +name = "flate2" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ - "quote", - "syn", + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", ] [[package]] -name = "event-listener" -version = "2.5.1" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fastrand" -version = "1.5.0" +name = "form_urlencoded" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ - "instant", + "matches", + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] -name = "futures-lite" -version = "1.12.0" +name = "futures-macro" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ - "fastrand", "futures-core", - "futures-io", - "memchr", - "parking", + "futures-macro", + "futures-sink", + "futures-task", "pin-project-lite", - "waker-fn", + "pin-utils", + "slab", ] [[package]] -name = "gloo-timers" -version = "0.2.1" +name = "generic-array" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" dependencies = [ - "futures-channel", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "git2" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7d3b96ec1fcaa8431cf04a4f1ef5caafe58d5cf7bcc31f09c1626adddb0ffe" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "h2" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +dependencies = [ + "bytes", + "fnv", "futures-core", - "js-sys", - "wasm-bindgen", - "web-sys", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util 0.6.9", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hdrhistogram" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31672b7011be2c4f7456c4ddbcb40e7e9a4a9fad8efe49a6ebaf5f307d0109c0" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", ] [[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -326,6 +542,123 @@ dependencies = [ ] [[package]] +name = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] name = "instant" version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -335,27 +668,47 @@ dependencies = [ ] [[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] name = "itoa" -version = "0.4.8" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] -name = "js-sys" -version = "0.3.55" +name = "jobserver" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" dependencies = [ - "wasm-bindgen", + "libc", ] [[package]] -name = "kv-log-macro" -version = "1.0.7" +name = "kqueue" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +checksum = "058a107a784f8be94c7d35c1300f4facced2e93d2fbe5b1452b44e905ddca4a9" dependencies = [ - "log", + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", ] [[package]] @@ -366,9 +719,42 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.107" +version = "0.2.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" +checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" + +[[package]] +name = "libgit2-sys" +version = "0.13.1+1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e598aa7a4faedf1ea1b4608f582b06f0f40211eec551b7ef36019ae3f62def" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libz-sys" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] [[package]] name = "log" @@ -377,16 +763,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if", - "value-bag", ] [[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] name = "match_cfg" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] name = "memchr" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -394,29 +791,86 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memoffset" -version = "0.6.4" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ + "adler", "autocfg", ] [[package]] +name = "mio" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] name = "nbsh" version = "0.1.0" dependencies = [ "anyhow", - "async-std", - "chrono", - "futures-lite", + "bincode", + "bytes", + "clap", + "console-subscriber", + "directories", + "futures-util", + "git2", + "glob", "hostname", "libc", "nix", + "notify", + "once_cell", + "pest", + "pest_derive", "pty-process", - "signal-hook", - "signal-hook-async-std", + "serde", "terminal_size", "textmode", + "time", + "tokio", + "tokio-stream", + "tokio-util 0.7.0", + "toml", "unicode-width", "users", "vt100", @@ -424,25 +878,50 @@ dependencies = [ [[package]] name = "nix" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188" +version = "0.23.1" +source = "git+https://github.com/nix-rust/nix#9312f1c410e7390f9ccb9ea8f255e09b4bb2a0ee" dependencies = [ "bitflags", - "cc", "cfg-if", "libc", "memoffset", ] [[package]] -name = "num-integer" -version = "0.1.44" +name = "nom" +version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" dependencies = [ - "autocfg", - "num-traits", + "memchr", + "minimal-lexical", + "version_check", +] + +[[package]] +name = "notify" +version = "5.0.0-pre.13" +source = "git+https://github.com/notify-rs/notify#b1802ecc8051b5c890ae05483513c75a71834f93" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", ] [[package]] @@ -456,31 +935,151 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", ] [[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + +[[package]] name = "once_cell" -version = "1.8.0" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] [[package]] -name = "parking" -version = "2.0.0" +name = "percent-encoding" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1", +] + +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -489,67 +1088,268 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "polling" -version = "2.2.0" +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ - "cfg-if", - "libc", - "log", - "wepoll-ffi", - "winapi", + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.32" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ "unicode-xid", ] [[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes", + "heck 0.3.3", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +dependencies = [ + "bytes", + "prost", +] + +[[package]] name = "pty-process" -version = "0.1.1" +version = "0.2.0" +source = "git+https://github.com/doy/pty-process#b2733e64d900ac237211360848326f3c4caa23f5" dependencies = [ - "async-io", - "async-process", "libc", "nix", - "thiserror", + "tokio", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] [[package]] -name = "signal-hook" -version = "0.3.10" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "signal-hook-registry", + "rand_chacha", + "rand_core", ] [[package]] -name = "signal-hook-async-std" -version = "0.2.1" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90526e74631c69a79b38212e3d4fda4b00de9d6be56b3cead133bf67ad371af1" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "async-io", - "futures-lite", - "libc", - "signal-hook", + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer", + "digest", + "fake-simd", + "opaque-debug", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", ] [[package]] @@ -568,20 +1368,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] name = "socket2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 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.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", @@ -589,6 +1401,29 @@ dependencies = [ ] [[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] name = "terminal_size" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -600,49 +1435,327 @@ dependencies = [ [[package]] name = "textmode" -version = "0.2.0" +version = "0.3.0" +source = "git+https://github.com/doy/textmode#193e1963afc4e9e78122573cd5b9831f9a847345" dependencies = [ - "blocking", - "futures-lite", "itoa", "nix", "terminal_size", - "thiserror", + "tokio", "vt100", ] [[package]] -name = "thiserror" -version = "1.0.30" +name = "textwrap" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" dependencies = [ - "thiserror-impl", + "terminal_size", ] [[package]] -name = "thiserror-impl" -version = "1.0.30" +name = "thread_local" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ - "proc-macro2", - "quote", - "syn", + "once_cell", ] [[package]] name = "time" -version = "0.1.44" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" dependencies = [ + "itoa", "libc", - "wasi", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", "winapi", ] [[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tonic" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" +dependencies = [ + "async-stream", + "async-trait", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util 0.6.9", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9403f1bafde247186684b230dc6f38b5cd514584e8bec1dd32514be4745fa757" +dependencies = [ + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util 0.7.0", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +dependencies = [ + "lazy_static", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] name = "unicode-width" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -655,6 +1768,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] name = "users" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -671,24 +1796,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" [[package]] -name = "value-bag" -version = "1.0.0-alpha.8" +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" -dependencies = [ - "ctor", - "version_check", -] +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vt100" -version = "0.13.0" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7541312ce0411d878458abf25175d878e8edc38f9f12ee8eed1d65870cacf540" dependencies = [ "itoa", "log", @@ -718,120 +1847,113 @@ dependencies = [ ] [[package]] -name = "waker-fn" -version = "1.1.0" +name = "walkdir" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] [[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +name = "want" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] [[package]] -name = "wasm-bindgen" -version = "0.2.78" +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] -name = "wasm-bindgen-backend" -version = "0.2.78" +name = "which" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" dependencies = [ - "bumpalo", + "either", "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "libc", ] [[package]] -name = "wasm-bindgen-futures" -version = "0.4.28" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.78" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.78" +name = "winapi-util" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", + "winapi", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.78" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "web-sys" -version = "0.3.55" +name = "windows-sys" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" dependencies = [ - "js-sys", - "wasm-bindgen", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", ] [[package]] -name = "wepoll-ffi" -version = "0.1.2" +name = "windows_aarch64_msvc" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows_i686_gnu" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows_i686_msvc" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows_x86_64_gnu" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" @@ -6,20 +6,42 @@ edition = "2021" license = "MIT" [dependencies] -anyhow = "1.0.45" -async-std = { version = "1.10.0", features = ["unstable"] } -chrono = "0.4.19" -futures-lite = "1.12.0" +anyhow = "1.0.55" +bincode = "1.3.3" +bytes = "1.1.0" +clap = { version = "3.1.5", features = ["wrap_help", "derive"] } +directories = "4.0.1" +futures-util = "0.3.21" +git2 = { version = "0.14.1", default-features = false } +glob = "0.3.0" hostname = "0.3.1" -libc = "0.2.107" -nix = "0.23.0" -pty-process = { path = "../pty-process", version = "0.1.1", features = ["backend-async-std"] } -signal-hook = "0.3.10" -signal-hook-async-std = "0.2.1" +libc = "0.2.119" +nix = "0.23.1" +notify = "5.0.0-pre.13" +once_cell = "1.10.0" +pest = "2.1.3" +pest_derive = "2.1.0" +pty-process = { version = "0.2.0", features = ["async"] } +serde = { version = "1.0.136", features = ["derive"] } terminal_size = "0.1.17" -textmode = { path = "../textmode", version = "0.2.0", features = ["async"] } +textmode = { version = "0.3.0", features = ["async"] } +time = { version = "0.3.7", features = ["formatting", "parsing"] } +tokio = { version = "1.17.0", features = ["full"] } +tokio-stream = { version = "0.1.8", features = ["io-util"] } +tokio-util = { version = "0.7.0", features = ["io"] } +toml = "0.5.8" unicode-width = "0.1.9" users = "0.11.0" -vt100 = { path = "../vt100-rust", version = "0.13.0" } +vt100 = "0.15.1" -[features] +[target.'cfg(nbsh_tokio_console)'.dependencies] +console-subscriber = "0.1.3" + +[patch.crates-io] +nix = { git = "https://github.com/nix-rust/nix" } +notify = { git = "https://github.com/notify-rs/notify" } +pty-process = { git = "https://github.com/doy/pty-process" } +textmode = { git = "https://github.com/doy/textmode" } + +[dev-dependencies] +time = { version = "0.3.7", features = ["macros"] } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f5641f2 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +build: + cargo build +.PHONY: build + +run: + cargo run +.PHONY: run + +console: + RUSTFLAGS="--cfg tokio_unstable --cfg nbsh_tokio_console" cargo run +.PHONY: console + +release: + cargo build --release +.PHONY: release + +run-release: + cargo run --release +.PHONY: run-release + +console-release: + RUSTFLAGS="--cfg tokio_unstable --cfg nbsh_tokio_console" cargo run --release +.PHONY: console-release @@ -10,5 +10,5 @@ unsound = "deny" [bans] [licenses] -allow = ["MIT", "Apache-2.0"] +allow = ["MIT", "Apache-2.0", "ISC", "CC0-1.0"] copyleft = "deny" diff --git a/src/action.rs b/src/action.rs deleted file mode 100644 index 22f088e..0000000 --- a/src/action.rs +++ /dev/null @@ -1,123 +0,0 @@ -#[derive(Debug)] -pub enum Action { - Render, - ForceRedraw, - Run(String), - UpdateFocus(crate::state::Focus), - ToggleFullscreen(usize), - Resize((u16, u16)), - Quit, -} - -pub struct Debouncer { - pending: async_std::sync::Mutex<Pending>, - cvar: async_std::sync::Condvar, -} - -impl Debouncer { - pub async fn recv(&self) -> Option<Action> { - let mut pending = self - .cvar - .wait_until(self.pending.lock().await, |pending| { - pending.has_event() - }) - .await; - pending.get_event() - } - - async fn send(&self, action: Option<Action>) { - let mut pending = self.pending.lock().await; - pending.new_event(&action); - self.cvar.notify_one(); - } -} - -#[derive(Default)] -struct Pending { - render: Option<()>, - force_redraw: Option<()>, - run: std::collections::VecDeque<String>, - focus: Option<crate::state::Focus>, - fullscreen: std::collections::VecDeque<usize>, - size: Option<(u16, u16)>, - done: bool, -} - -impl Pending { - fn new() -> Self { - Self::default() - } - - fn has_event(&self) -> bool { - self.done - || self.render.is_some() - || self.force_redraw.is_some() - || !self.run.is_empty() - || self.focus.is_some() - || !self.fullscreen.is_empty() - || self.size.is_some() - } - - fn get_event(&mut self) -> Option<Action> { - if self.size.is_some() { - return Some(Action::Resize(self.size.take().unwrap())); - } - if !self.run.is_empty() { - return Some(Action::Run(self.run.pop_front().unwrap())); - } - if self.focus.is_some() { - return Some(Action::UpdateFocus(self.focus.take().unwrap())); - } - if !self.fullscreen.is_empty() { - return Some(Action::ToggleFullscreen( - self.fullscreen.pop_front().unwrap(), - )); - } - if self.force_redraw.is_some() { - self.force_redraw.take(); - self.render.take(); - return Some(Action::ForceRedraw); - } - if self.render.is_some() { - self.render.take(); - return Some(Action::Render); - } - if self.done { - return None; - } - unreachable!() - } - - fn new_event(&mut self, action: &Option<Action>) { - match action { - Some(Action::Render) => self.render = Some(()), - Some(Action::ForceRedraw) => self.force_redraw = Some(()), - Some(Action::Run(cmd)) => self.run.push_back(cmd.to_string()), - Some(Action::UpdateFocus(focus)) => self.focus = Some(*focus), - Some(Action::ToggleFullscreen(idx)) => { - self.fullscreen.push_back(*idx); - } - Some(Action::Resize(size)) => self.size = Some(*size), - Some(Action::Quit) | None => self.done = true, - } - } -} - -pub fn debounce( - input: async_std::channel::Receiver<Action>, -) -> async_std::sync::Arc<Debouncer> { - let debouncer = std::sync::Arc::new(Debouncer { - pending: async_std::sync::Mutex::new(Pending::new()), - cvar: async_std::sync::Condvar::new(), - }); - { - let debouncer = std::sync::Arc::clone(&debouncer); - async_std::task::spawn(async move { - while let Ok(action) = input.recv().await { - debouncer.send(Some(action)).await; - } - debouncer.send(None).await; - }); - } - debouncer -} diff --git a/src/builtins.rs b/src/builtins.rs deleted file mode 100644 index 225ef5b..0000000 --- a/src/builtins.rs +++ /dev/null @@ -1,44 +0,0 @@ -pub fn is(exe: &str) -> bool { - matches!(exe, "cd") -} - -pub fn run(exe: &str, args: &[String]) -> u8 { - match exe { - "cd" => { - impls::cd(args.iter().map(|s| s.as_ref()).next().unwrap_or("")) - } - _ => unreachable!(), - } -} - -mod impls { - pub fn cd(dir: &str) -> u8 { - let dir = if dir.is_empty() { - home() - } else if dir.starts_with('~') { - let path: std::path::PathBuf = dir.into(); - if let std::path::Component::Normal(prefix) = - path.components().next().unwrap() - { - if prefix.to_str() == Some("~") { - home().join(path.strip_prefix(prefix).unwrap()) - } else { - // TODO - return 1; - } - } else { - unreachable!() - } - } else { - dir.into() - }; - match std::env::set_current_dir(dir) { - Ok(()) => 0, - Err(_) => 1, - } - } - - fn home() -> std::path::PathBuf { - std::env::var_os("HOME").unwrap().into() - } -} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..08fa002 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,25 @@ +use crate::prelude::*; + +#[derive(serde::Deserialize, Default, Debug)] +pub struct Config { + aliases: + std::collections::HashMap<std::path::PathBuf, crate::parse::ast::Exe>, +} + +impl Config { + pub fn load() -> Result<Self> { + let file = crate::dirs::config_file(); + if std::fs::metadata(&file).is_ok() { + Ok(toml::from_slice(&std::fs::read(&file)?)?) + } else { + Ok(Self::default()) + } + } + + pub fn alias_for( + &self, + path: &std::path::Path, + ) -> Option<&crate::parse::ast::Exe> { + self.aliases.get(path) + } +} diff --git a/src/dirs.rs b/src/dirs.rs new file mode 100644 index 0000000..2ffbb33 --- /dev/null +++ b/src/dirs.rs @@ -0,0 +1,20 @@ +static PROJECT_DIRS: once_cell::sync::Lazy<directories::ProjectDirs> = + once_cell::sync::Lazy::new(|| { + directories::ProjectDirs::from("", "", "nbsh").unwrap() + }); + +pub fn config_file() -> std::path::PathBuf { + config_dir().join("config.toml") +} + +pub fn history_file() -> std::path::PathBuf { + data_dir().join("history") +} + +fn config_dir() -> std::path::PathBuf { + PROJECT_DIRS.config_dir().to_path_buf() +} + +fn data_dir() -> std::path::PathBuf { + PROJECT_DIRS.data_dir().to_path_buf() +} diff --git a/src/env.rs b/src/env.rs new file mode 100644 index 0000000..72a69d1 --- /dev/null +++ b/src/env.rs @@ -0,0 +1,151 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub enum Env { + V0(V0), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct V0 { + pwd: std::path::PathBuf, + vars: std::collections::HashMap<std::ffi::OsString, std::ffi::OsString>, +} + +const __NBSH_IDX: &str = "__NBSH_IDX"; +const __NBSH_LATEST_STATUS: &str = "__NBSH_LATEST_STATUS"; +const __NBSH_PREV_PWD: &str = "__NBSH_PREV_PWD"; + +impl Env { + pub fn new() -> Result<Self> { + let pwd = std::env::current_dir()?; + Ok(Self::V0(V0 { + pwd: pwd.clone(), + vars: std::env::vars_os() + .chain(Self::defaults(pwd).into_iter()) + .collect(), + })) + } + + pub fn new_from_env() -> Result<Self> { + let pwd = std::env::current_dir()?; + Ok(Self::V0(V0 { + pwd: pwd.clone(), + vars: Self::defaults(pwd) + .into_iter() + .chain(std::env::vars_os()) + .collect(), + })) + } + + pub fn pwd(&self) -> &std::path::Path { + match self { + Self::V0(env) => &env.pwd, + } + } + + pub fn var(&self, k: &str) -> Option<String> { + match self { + Self::V0(env) => self.special_var(k).or_else(|| { + env.vars + .get(std::ffi::OsStr::new(k)) + .map(|v| v.to_str().unwrap().to_string()) + }), + } + } + + pub fn set_var< + K: Into<std::ffi::OsString>, + V: Into<std::ffi::OsString>, + >( + &mut self, + k: K, + v: V, + ) { + match self { + Self::V0(env) => { + env.vars.insert(k.into(), v.into()); + } + } + } + + pub fn idx(&self) -> usize { + self.var(__NBSH_IDX).unwrap().parse().unwrap() + } + + pub fn set_idx(&mut self, idx: usize) { + self.set_var(__NBSH_IDX, format!("{}", idx)); + } + + pub fn latest_status(&self) -> std::process::ExitStatus { + std::process::ExitStatus::from_raw( + self.var(__NBSH_LATEST_STATUS).unwrap().parse().unwrap(), + ) + } + + pub fn set_status(&mut self, status: std::process::ExitStatus) { + self.set_var( + __NBSH_LATEST_STATUS, + format!( + "{}", + (status.code().unwrap_or(0) << 8) + | status.signal().unwrap_or(0) + ), + ); + } + + pub fn prev_pwd(&self) -> std::path::PathBuf { + std::path::PathBuf::from(self.var(__NBSH_PREV_PWD).unwrap()) + } + + pub fn set_prev_pwd(&mut self, prev_pwd: std::path::PathBuf) { + self.set_var(__NBSH_PREV_PWD, prev_pwd); + } + + pub fn apply(&self, cmd: &mut pty_process::Command) { + match self { + Self::V0(env) => { + cmd.current_dir(&env.pwd); + cmd.env_clear(); + cmd.envs(env.vars.iter()); + } + } + } + + pub fn update(&mut self) -> Result<()> { + let idx = self.idx(); + let status = self.latest_status(); + let prev_pwd = self.prev_pwd(); + *self = Self::new()?; + self.set_idx(idx); + self.set_status(status); + self.set_prev_pwd(prev_pwd); + Ok(()) + } + + fn special_var(&self, k: &str) -> Option<String> { + Some(match k { + "$" => crate::info::pid(), + "?" => { + let status = self.latest_status(); + status + .signal() + .map_or_else( + || status.code().unwrap(), + |signal| signal + 128, + ) + .to_string() + } + _ => return None, + }) + } + + fn defaults( + pwd: std::path::PathBuf, + ) -> [(std::ffi::OsString, std::ffi::OsString); 3] { + [ + (__NBSH_IDX.into(), "0".into()), + (__NBSH_LATEST_STATUS.into(), "0".into()), + (__NBSH_PREV_PWD.into(), pwd.into()), + ] + } +} diff --git a/src/format.rs b/src/format.rs index 50424f0..115ee6c 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,15 +1,39 @@ -use std::os::unix::process::ExitStatusExt as _; +use crate::prelude::*; -pub fn exit_status(status: std::process::ExitStatus) -> String { - if let Some(sig) = status.signal() { - if let Some(name) = signal_hook::low_level::signal_name(sig) { - format!("{:4} ", &name[3..]) - } else { - format!("SIG{} ", sig) +pub fn path(path: &std::path::Path) -> String { + let mut path = path.display().to_string(); + if let Ok(home) = std::env::var("HOME") { + if path.starts_with(&home) { + path.replace_range(..home.len(), "~"); } - } else { - format!("{:03} ", status.code().unwrap()) } + path +} + +pub fn exit_status(status: std::process::ExitStatus) -> String { + status.signal().map_or_else( + || format!("{:03} ", status.code().unwrap()), + |sig| { + nix::sys::signal::Signal::try_from(sig).map_or_else( + |_| format!("SIG{} ", sig), + |sig| format!("{:4} ", &sig.as_str()[3..]), + ) + }, + ) +} + +pub fn time(time: time::OffsetDateTime) -> String { + let format = if time::OffsetDateTime::now_utc() - time + > std::time::Duration::from_secs(60 * 60 * 24) + { + time::format_description::parse( + "[year]-[month]-[day] [hour]:[minute]:[second]", + ) + .unwrap() + } else { + time::format_description::parse("[hour]:[minute]:[second]").unwrap() + }; + time.format(&format).unwrap() } pub fn duration(dur: std::time::Duration) -> String { @@ -31,3 +55,12 @@ pub fn duration(dur: std::time::Duration) -> String { format!("{}ns", nanos) } } + +pub fn io_error(e: &std::io::Error) -> String { + let mut s = format!("{}", e); + if e.raw_os_error().is_some() { + let i = s.rfind('(').unwrap(); + s.truncate(i - 1); + } + s +} diff --git a/src/history.pest b/src/history.pest new file mode 100644 index 0000000..67597d1 --- /dev/null +++ b/src/history.pest @@ -0,0 +1,5 @@ +time = @{ ASCII_DIGIT+ } +duration = @{ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? } +command = @{ ANY* } + +line = ${ SOI ~ (": " ~ time ~ ":" ~ duration ~ ";")? ~ command ~ "\n"? ~ EOI } diff --git a/src/history.rs b/src/history.rs deleted file mode 100644 index d6f198a..0000000 --- a/src/history.rs +++ /dev/null @@ -1,407 +0,0 @@ -use async_std::io::{ReadExt as _, WriteExt as _}; -use futures_lite::future::FutureExt as _; -use pty_process::Command as _; -use std::os::unix::process::ExitStatusExt as _; -use textmode::Textmode as _; - -pub struct History { - size: (u16, u16), - entries: Vec<crate::util::Mutex<HistoryEntry>>, -} - -impl History { - pub fn new() -> Self { - Self { - size: (24, 80), - entries: vec![], - } - } - - pub async fn handle_key(&self, key: textmode::Key, idx: usize) { - let entry = self.entries[idx].lock_arc().await; - if entry.running() { - entry.input.send(key.into_bytes()).await.unwrap(); - } - } - - pub async fn render( - &self, - out: &mut textmode::Output, - repl_lines: usize, - focus: Option<usize>, - ) -> anyhow::Result<()> { - if let Some(idx) = focus { - let mut entry = self.entries[idx].lock_arc().await; - if entry.should_fullscreen() { - entry.render_fullscreen(out); - return Ok(()); - } - } - - let mut used_lines = repl_lines; - let mut pos = None; - for (idx, entry) in self.entries.iter().enumerate().rev() { - let mut entry = entry.lock_arc().await; - let focused = focus.map_or(false, |focus| idx == focus); - let last_row = entry.lines(self.size.1, focused); - used_lines += 1 + std::cmp::min(6, last_row); - if used_lines > self.size.0 as usize { - break; - } - if focused && used_lines == 1 && entry.running() { - used_lines = 2; - } - out.move_to( - (self.size.0 as usize - used_lines).try_into().unwrap(), - 0, - ); - entry.render(out, self.size.1, focused); - if focused { - pos = Some(out.screen().cursor_position()); - } - } - if let Some(pos) = pos { - out.move_to(pos.0, pos.1); - } - Ok(()) - } - - pub async fn resize(&mut self, size: (u16, u16)) { - self.size = size; - for entry in &self.entries { - let entry = entry.lock_arc().await; - if entry.running() { - entry.resize.send(size).await.unwrap(); - } - } - } - - pub async fn run( - &mut self, - cmd: &str, - action_w: async_std::channel::Sender<crate::action::Action>, - ) -> anyhow::Result<usize> { - let (exe, args) = crate::parse::cmd(cmd); - let (input_w, input_r) = async_std::channel::unbounded(); - let (resize_w, resize_r) = async_std::channel::unbounded(); - let entry = crate::util::mutex(HistoryEntry::new( - cmd, self.size, input_w, resize_w, - )); - if crate::builtins::is(&exe) { - let code: i32 = crate::builtins::run(&exe, &args).into(); - entry.lock_arc().await.exit_info = Some(ExitInfo::new( - async_std::process::ExitStatus::from_raw(code << 8), - )); - action_w - .send(crate::action::Action::UpdateFocus( - crate::state::Focus::Readline, - )) - .await - .unwrap(); - } else { - let mut process = async_std::process::Command::new(&exe); - process.args(&args); - let child = process - .spawn_pty(Some(&pty_process::Size::new( - self.size.0, - self.size.1, - ))) - .unwrap(); - run_process( - child, - async_std::sync::Arc::clone(&entry), - input_r, - resize_r, - action_w, - ); - } - self.entries.push(entry); - Ok(self.entries.len() - 1) - } - - pub async fn toggle_fullscreen(&mut self, idx: usize) { - self.entries[idx].lock_arc().await.toggle_fullscreen(); - } - - pub async fn is_fullscreen(&self, idx: usize) -> bool { - self.entries[idx].lock_arc().await.should_fullscreen() - } - - pub fn entry_count(&self) -> usize { - self.entries.len() - } -} - -struct HistoryEntry { - cmd: String, - vt: vt100::Parser, - audible_bell_state: usize, - visual_bell_state: usize, - fullscreen: Option<bool>, - input: async_std::channel::Sender<Vec<u8>>, - resize: async_std::channel::Sender<(u16, u16)>, - start_time: chrono::DateTime<chrono::Local>, - start_instant: std::time::Instant, - exit_info: Option<ExitInfo>, -} - -impl HistoryEntry { - fn new( - cmd: &str, - size: (u16, u16), - input: async_std::channel::Sender<Vec<u8>>, - resize: async_std::channel::Sender<(u16, u16)>, - ) -> Self { - Self { - cmd: cmd.into(), - vt: vt100::Parser::new(size.0, size.1, 0), - audible_bell_state: 0, - visual_bell_state: 0, - input, - resize, - fullscreen: None, - start_time: chrono::Local::now(), - start_instant: std::time::Instant::now(), - exit_info: None, - } - } - - fn render( - &mut self, - out: &mut textmode::Output, - width: u16, - focused: bool, - ) { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - if let Some(info) = self.exit_info { - if info.status.signal().is_some() { - out.set_fgcolor(textmode::color::MAGENTA); - } else if info.status.success() { - out.set_fgcolor(textmode::color::DARKGREY); - } else { - out.set_fgcolor(textmode::color::RED); - } - out.write_str(&crate::format::exit_status(info.status)); - } else { - out.write_str(" "); - } - out.reset_attributes(); - if focused { - out.set_fgcolor(textmode::color::BLACK); - out.set_bgcolor(textmode::color::CYAN); - } else { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - } - out.write_str("$ "); - out.reset_attributes(); - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - if self.running() { - out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); - } - out.write_str(&self.cmd); - out.reset_attributes(); - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - let time = if let Some(info) = self.exit_info { - format!( - "[{} ({:6})]", - self.start_time.time().format("%H:%M:%S"), - crate::format::duration(info.instant - self.start_instant) - ) - } else { - format!("[{}]", self.start_time.time().format("%H:%M:%S")) - }; - let cur_pos = out.screen().cursor_position(); - out.write_str( - &" ".repeat(width as usize - time.len() - 1 - cur_pos.1 as usize), - ); - out.write_str(&time); - out.write_str(" "); - out.reset_attributes(); - let last_row = self.lines(width, focused); - if last_row > 5 { - out.write(b"\r\n"); - out.set_fgcolor(textmode::color::BLUE); - out.write(b"..."); - out.reset_attributes(); - } - let mut out_row = out.screen().cursor_position().0 + 1; - let screen = self.vt.screen(); - let pos = screen.cursor_position(); - let mut wrapped = false; - let mut cursor_found = None; - for (idx, row) in screen - .rows_formatted(0, width) - .enumerate() - .take(last_row) - .skip(last_row.saturating_sub(5)) - { - let idx: u16 = idx.try_into().unwrap(); - out.write(b"\x1b[m"); - if !wrapped { - out.write(format!("\x1b[{}H", out_row + 1).as_bytes()); - } - out.write(&row); - wrapped = screen.row_wrapped(idx); - if pos.0 == idx { - cursor_found = Some(out_row); - } - out_row += 1; - } - if focused { - if let Some(row) = cursor_found { - if screen.hide_cursor() { - out.write(b"\x1b[?25l"); - } else { - out.write(b"\x1b[?25h"); - out.move_to(row, pos.1); - } - } else { - out.write(b"\x1b[?25l"); - } - } - out.reset_attributes(); - } - - fn render_fullscreen(&mut self, out: &mut textmode::Output) { - let screen = self.vt.screen(); - let new_audible_bell_state = screen.audible_bell_count(); - let new_visual_bell_state = screen.visual_bell_count(); - - out.write(&screen.state_formatted()); - - if self.audible_bell_state != new_audible_bell_state { - out.write(b"\x07"); - self.audible_bell_state = new_audible_bell_state; - } - - if self.visual_bell_state != new_visual_bell_state { - out.write(b"\x1bg"); - self.visual_bell_state = new_visual_bell_state; - } - - out.reset_attributes(); - } - - fn toggle_fullscreen(&mut self) { - if let Some(fullscreen) = self.fullscreen { - self.fullscreen = Some(!fullscreen); - } else { - self.fullscreen = Some(!self.vt.screen().alternate_screen()); - } - } - - fn running(&self) -> bool { - self.exit_info.is_none() - } - - fn lines(&self, width: u16, focused: bool) -> usize { - let screen = self.vt.screen(); - let mut last_row = 0; - for (idx, row) in screen.rows(0, width).enumerate() { - if !row.is_empty() { - last_row = idx + 1; - } - } - if focused && self.running() { - last_row = std::cmp::max( - last_row, - screen.cursor_position().0 as usize + 1, - ); - } - last_row - } - - fn should_fullscreen(&self) -> bool { - self.fullscreen - .unwrap_or_else(|| self.vt.screen().alternate_screen()) - } -} - -#[derive(Copy, Clone)] -struct ExitInfo { - status: async_std::process::ExitStatus, - instant: std::time::Instant, -} - -impl ExitInfo { - fn new(status: async_std::process::ExitStatus) -> Self { - Self { - status, - instant: std::time::Instant::now(), - } - } -} - -fn run_process( - mut child: pty_process::async_std::Child, - entry: crate::util::Mutex<HistoryEntry>, - input_r: async_std::channel::Receiver<Vec<u8>>, - resize_r: async_std::channel::Receiver<(u16, u16)>, - action_w: async_std::channel::Sender<crate::action::Action>, -) { - async_std::task::spawn(async move { - loop { - enum Res { - Read(Result<usize, std::io::Error>), - Write(Result<Vec<u8>, async_std::channel::RecvError>), - Resize(Result<(u16, u16), async_std::channel::RecvError>), - } - let mut buf = [0_u8; 4096]; - let mut pty = child.pty(); - let read = async { Res::Read(pty.read(&mut buf).await) }; - let write = async { Res::Write(input_r.recv().await) }; - let resize = async { Res::Resize(resize_r.recv().await) }; - match read.race(write).race(resize).await { - Res::Read(res) => { - match res { - Ok(bytes) => { - entry.lock_arc().await.vt.process(&buf[..bytes]); - } - Err(e) => { - if e.raw_os_error() != Some(libc::EIO) { - eprintln!("pty read failed: {:?}", e); - } - // XXX not sure if this is safe - are we sure - // the child exited? - entry.lock_arc().await.exit_info = Some( - ExitInfo::new(child.status().await.unwrap()), - ); - action_w - .send(crate::action::Action::UpdateFocus( - crate::state::Focus::Readline, - )) - .await - .unwrap(); - break; - } - } - action_w - .send(crate::action::Action::Render) - .await - .unwrap(); - } - Res::Write(res) => match res { - Ok(bytes) => { - pty.write(&bytes).await.unwrap(); - } - Err(e) => { - panic!("failed to read from input channel: {}", e); - } - }, - Res::Resize(res) => match res { - Ok(size) => { - child - .resize_pty(&pty_process::Size::new( - size.0, size.1, - )) - .unwrap(); - entry.lock_arc().await.vt.set_size(size.0, size.1); - } - Err(e) => { - panic!("failed to read from resize channel: {}", e); - } - }, - } - } - }); -} diff --git a/src/info.rs b/src/info.rs new file mode 100644 index 0000000..6a5ad4f --- /dev/null +++ b/src/info.rs @@ -0,0 +1,67 @@ +use crate::prelude::*; + +pub fn user() -> Result<String> { + Ok(users::get_current_username() + .ok_or_else(|| anyhow!("couldn't get username"))? + .to_string_lossy() + .into_owned()) +} + +#[allow(clippy::unnecessary_wraps)] +pub fn prompt_char() -> Result<String> { + if users::get_current_uid() == 0 { + Ok("#".into()) + } else { + Ok("$".into()) + } +} + +pub fn hostname() -> Result<String> { + let mut hostname = hostname::get()?.to_string_lossy().into_owned(); + if let Some(idx) = hostname.find('.') { + hostname.truncate(idx); + } + Ok(hostname) +} + +#[allow(clippy::unnecessary_wraps)] +pub fn time(offset: time::UtcOffset) -> Result<String> { + Ok(crate::format::time( + time::OffsetDateTime::now_utc().to_offset(offset), + )) +} + +pub fn pid() -> String { + nix::unistd::getpid().to_string() +} + +#[cfg(target_os = "linux")] +#[allow(clippy::unnecessary_wraps)] +pub fn current_exe() -> Result<std::path::PathBuf> { + Ok("/proc/self/exe".into()) +} + +#[cfg(not(target_os = "linux"))] +pub fn current_exe() -> Result<std::path::PathBuf> { + Ok(std::env::current_exe()?) +} + +// the time crate is currently unable to get the local offset on unix due to +// soundness concerns, so we have to do it manually/: +// +// https://github.com/time-rs/time/issues/380 +pub fn get_offset() -> time::UtcOffset { + let offset_str = + std::process::Command::new("date").args(&["+%:z"]).output(); + if let Ok(offset_str) = offset_str { + let offset_str = String::from_utf8(offset_str.stdout).unwrap(); + time::UtcOffset::parse( + offset_str.trim(), + &time::format_description::parse("[offset_hour]:[offset_minute]") + .unwrap(), + ) + .unwrap_or(time::UtcOffset::UTC) + } else { + time::UtcOffset::UTC + } +} diff --git a/src/main.rs b/src/main.rs index 94d6f90..d6b2725 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,113 +1,70 @@ +// will uncomment this once it is closer to release +// #![warn(clippy::cargo)] #![warn(clippy::pedantic)] #![warn(clippy::nursery)] +#![warn(clippy::as_conversions)] +#![warn(clippy::get_unwrap)] +#![allow(clippy::cognitive_complexity)] #![allow(clippy::missing_const_for_fn)] +#![allow(clippy::option_option)] +#![allow(clippy::similar_names)] +#![allow(clippy::struct_excessive_bools)] +#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_lines)] -#![allow(clippy::unused_self)] +#![allow(clippy::type_complexity)] +// this isn't super relevant in a binary - if it's actually a problem, we'll +// just get a compilation failure +#![allow(clippy::future_not_send)] -mod action; -mod builtins; +mod config; +mod dirs; +mod env; mod format; -mod history; +mod info; mod parse; -mod readline; -mod state; -mod util; +mod prelude; +mod runner; +mod shell; -use async_std::stream::StreamExt as _; +use prelude::*; -async fn resize( - action_w: &async_std::channel::Sender<crate::action::Action>, -) { - let size = terminal_size::terminal_size().map_or( - (24, 80), - |(terminal_size::Width(w), terminal_size::Height(h))| (h, w), - ); - action_w - .send(crate::action::Action::Resize(size)) - .await - .unwrap(); -} - -async fn async_main() -> anyhow::Result<()> { - let mut input = textmode::Input::new().await?; - let mut output = textmode::Output::new().await?; - - // avoid the guards getting stuck in a task that doesn't run to - // completion - let _input_guard = input.take_raw_guard(); - let _output_guard = output.take_screen_guard(); - - let (action_w, action_r) = async_std::channel::unbounded(); +use clap::Parser as _; - let state = state::State::new(); - state.render(&mut output, true).await.unwrap(); +#[derive(clap::Parser)] +#[clap(about = "NoteBook SHell")] +struct Opt { + #[clap(short = 'c')] + command: Option<String>, - let state = util::mutex(state); - - { - let mut signals = signal_hook_async_std::Signals::new(&[ - signal_hook::consts::signal::SIGWINCH, - ])?; - let action_w = action_w.clone(); - async_std::task::spawn(async move { - while signals.next().await.is_some() { - resize(&action_w).await; - } - }); - } - - resize(&action_w).await; + #[clap(long)] + status_fd: Option<std::os::unix::io::RawFd>, +} - { - let state = async_std::sync::Arc::clone(&state); - let action_w = action_w.clone(); - async_std::task::spawn(async move { - while let Some(key) = input.read_key().await.unwrap() { - if let Some(action) = - state.lock_arc().await.handle_key(key).await - { - action_w.send(action).await.unwrap(); - } - } +#[tokio::main] +async fn async_main(opt: Opt) -> Result<i32> { + if let Some(command) = opt.command { + let mut shell_write = opt.status_fd.and_then(|fd| { + nix::sys::stat::fstat(fd).ok().map(|_| { + // Safety: we don't create File instances for or read/write + // data on this fd anywhere else + unsafe { tokio::fs::File::from_raw_fd(fd) } + }) }); - } - // redraw the clock every second - { - let action_w = action_w.clone(); - async_std::task::spawn(async move { - let first_sleep = 1_000_000_000_u64.saturating_sub( - chrono::Local::now().timestamp_subsec_nanos().into(), - ); - async_std::task::sleep(std::time::Duration::from_nanos( - first_sleep, - )) - .await; - let mut interval = async_std::stream::interval( - std::time::Duration::from_secs(1), - ); - action_w.send(crate::action::Action::Render).await.unwrap(); - while interval.next().await.is_some() { - action_w.send(crate::action::Action::Render).await.unwrap(); - } - }); + return runner::main(command, &mut shell_write).await; } - let debouncer = crate::action::debounce(action_r); - while let Some(action) = debouncer.recv().await { - state - .lock_arc() - .await - .handle_action(action, &mut output, &action_w) - .await; - } + #[cfg(nbsh_tokio_console)] + console_subscriber::init(); - Ok(()) + shell::main().await } fn main() { - match async_std::task::block_on(async_main()) { - Ok(_) => (), + match async_main(Opt::parse()) { + Ok(code) => { + std::process::exit(code); + } Err(e) => { eprintln!("nbsh: {}", e); std::process::exit(1); diff --git a/src/parse.rs b/src/parse.rs deleted file mode 100644 index 84e8daa..0000000 --- a/src/parse.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub fn cmd(full_cmd: &str) -> (String, Vec<String>) { - let mut parts = full_cmd.split(' '); - let cmd = parts.next().unwrap(); - ( - cmd.to_string(), - parts.map(std::string::ToString::to_string).collect(), - ) -} diff --git a/src/parse/ast.rs b/src/parse/ast.rs new file mode 100644 index 0000000..5bceed5 --- /dev/null +++ b/src/parse/ast.rs @@ -0,0 +1,600 @@ +use crate::prelude::*; + +use pest::Parser as _; + +#[derive(pest_derive::Parser)] +#[grammar = "shell.pest"] +struct Shell; + +#[derive(Debug, PartialEq, Eq)] +pub struct Commands { + commands: Vec<Command>, +} + +impl Commands { + pub fn parse(full_cmd: &str) -> Result<Self, super::Error> { + Ok(Self::build_ast( + Shell::parse(Rule::line, full_cmd) + .map_err(|e| super::Error::new(full_cmd.to_string(), e))? + .next() + .unwrap() + .into_inner() + .next() + .unwrap(), + )) + } + + pub fn commands(&self) -> &[Command] { + &self.commands + } + + fn build_ast(commands: pest::iterators::Pair<Rule>) -> Self { + assert!(matches!(commands.as_rule(), Rule::commands)); + Self { + commands: commands.into_inner().map(Command::build_ast).collect(), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Command { + Pipeline(Pipeline), + If(Pipeline), + While(Pipeline), + For(String, Vec<Word>), + Else(Option<Pipeline>), + End, +} + +impl Command { + fn build_ast(command: pest::iterators::Pair<Rule>) -> Self { + assert!(matches!(command.as_rule(), Rule::command)); + let next = command.into_inner().next().unwrap(); + match next.as_rule() { + Rule::pipeline => Self::Pipeline(Pipeline::build_ast(next)), + Rule::control => { + let ty = next.into_inner().next().unwrap(); + match ty.as_rule() { + Rule::control_if => Self::If(Pipeline::build_ast( + ty.into_inner().next().unwrap(), + )), + Rule::control_while => Self::While(Pipeline::build_ast( + ty.into_inner().next().unwrap(), + )), + Rule::control_for => { + let mut inner = ty.into_inner(); + let var = inner.next().unwrap(); + assert!(matches!(var.as_rule(), Rule::bareword)); + let list = inner.next().unwrap(); + assert!(matches!(list.as_rule(), Rule::list)); + let vals = + list.into_inner().map(Word::build_ast).collect(); + Self::For(var.as_str().to_string(), vals) + } + Rule::control_else => Self::Else( + ty.into_inner().next().map(Pipeline::build_ast), + ), + Rule::control_end => Self::End, + _ => unreachable!(), + } + } + _ => unreachable!(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pipeline { + exes: Vec<Exe>, + span: (usize, usize), +} + +impl Pipeline { + pub async fn eval(self, env: &Env) -> Result<super::Pipeline> { + Ok(super::Pipeline { + exes: self + .exes + .into_iter() + .map(|exe| exe.eval(env)) + .collect::<futures_util::stream::FuturesOrdered<_>>() + .try_collect() + .await?, + }) + } + + pub fn span(&self) -> (usize, usize) { + self.span + } + + fn build_ast(pipeline: pest::iterators::Pair<Rule>) -> Self { + assert!(matches!(pipeline.as_rule(), Rule::pipeline)); + let span = (pipeline.as_span().start(), pipeline.as_span().end()); + Self { + exes: pipeline.into_inner().map(Exe::build_ast).collect(), + span, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Exe { + exe: Word, + args: Vec<Word>, + redirects: Vec<Redirect>, +} + +impl Exe { + pub async fn eval(self, env: &Env) -> Result<super::Exe> { + let exe = self.exe.eval(env).await?; + assert_eq!(exe.len(), 1); // TODO + let exe = &exe[0]; + Ok(super::Exe { + exe: std::path::PathBuf::from(exe), + args: self + .args + .into_iter() + .map(|arg| async { + arg.eval(env).await.map(IntoIterator::into_iter) + }) + .collect::<futures_util::stream::FuturesOrdered<_>>() + .try_collect::<Vec<_>>() + .await? + .into_iter() + .flatten() + .collect(), + redirects: self + .redirects + .into_iter() + .map(|arg| arg.eval(env)) + .collect::<futures_util::stream::FuturesOrdered<_>>() + .try_collect() + .await?, + }) + } + + pub fn parse(s: &str) -> Result<Self, super::Error> { + Ok(Self::build_ast( + Shell::parse(Rule::exe, s) + .map_err(|e| super::Error::new(s.to_string(), e))? + .next() + .unwrap(), + )) + } + + fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self { + assert!(matches!(pair.as_rule(), Rule::subshell | Rule::exe)); + if matches!(pair.as_rule(), Rule::subshell) { + let mut iter = pair.into_inner(); + let commands = iter.next().unwrap(); + assert!(matches!(commands.as_rule(), Rule::commands)); + let redirects = iter.map(Redirect::build_ast).collect(); + return Self { + exe: Word { + parts: vec![WordPart::SingleQuoted( + crate::info::current_exe() + .unwrap() + .to_str() + .unwrap() + .to_string(), + )], + }, + args: vec![ + Word { + parts: vec![WordPart::SingleQuoted("-c".to_string())], + }, + Word { + parts: vec![WordPart::SingleQuoted( + commands.as_str().to_string(), + )], + }, + ], + redirects, + }; + } + let mut iter = pair.into_inner(); + let exe = iter.next().unwrap(); + let exe = match exe.as_rule() { + Rule::word => Word::build_ast(exe), + Rule::redirect => todo!(), + _ => unreachable!(), + }; + let mut args = vec![]; + let mut redirects = vec![]; + for arg in iter { + match arg.as_rule() { + Rule::word => args.push(Word::build_ast(arg)), + Rule::redirect => redirects.push(Redirect::build_ast(arg)), + _ => unreachable!(), + } + } + Self { + exe, + args, + redirects, + } + } +} + +impl<'de> serde::Deserialize<'de> for Exe { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Exe; + + fn expecting( + &self, + f: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + f.write_str("a command") + } + + fn visit_str<E>( + self, + value: &str, + ) -> std::result::Result<Self::Value, E> + where + E: serde::de::Error, + { + Exe::parse(value).map_err(serde::de::Error::custom) + } + } + deserializer.deserialize_string(Visitor) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Word { + parts: Vec<WordPart>, +} + +impl Word { + pub async fn eval(self, env: &Env) -> Result<Vec<String>> { + let mut opts = glob::MatchOptions::new(); + opts.require_literal_separator = true; + opts.require_literal_leading_dot = true; + + let mut alternations: Vec<Vec<Vec<WordPart>>> = vec![]; + let mut cur: Vec<WordPart> = vec![]; + for part in self.parts { + if let WordPart::Alternation(words) = part { + if !cur.is_empty() { + alternations.push(vec![cur.clone()]); + cur.clear(); + } + alternations + .push(words.into_iter().map(|word| word.parts).collect()); + } else { + cur.push(part.clone()); + } + } + if !cur.is_empty() { + alternations.push(vec![cur]); + } + let mut words: Vec<Vec<WordPart>> = std::iter::repeat(vec![]) + .take(alternations.iter().map(Vec::len).product()) + .collect(); + for i in 0..words.len() { + let mut len = words.len(); + for alternation in &alternations { + let idx = (i * alternation.len() / len) % alternation.len(); + words[i].extend(alternation[idx].clone().into_iter()); + len /= alternation.len(); + } + } + + let mut expanded_words = vec![]; + for word in words { + let mut s = String::new(); + let mut pat = String::new(); + let mut is_glob = false; + let initial_bareword = word + .get(0) + .map_or(false, |part| matches!(part, WordPart::Bareword(_))); + for part in word { + match part { + WordPart::Alternation(_) => unreachable!(), + WordPart::Bareword(_) => { + let part = part.eval(env).await; + s.push_str(&part); + pat.push_str(&part); + if part.contains(&['*', '?', '['][..]) { + is_glob = true; + } + } + WordPart::Substitution(_) + | WordPart::Var(_) + | WordPart::DoubleQuoted(_) + | WordPart::SingleQuoted(_) => { + let part = part.eval(env).await; + s.push_str(&part); + pat.push_str(&glob::Pattern::escape(&part)); + } + } + } + if initial_bareword { + s = expand_home(&s)?; + pat = expand_home(&pat)?; + } + if is_glob { + let mut found = false; + for file in glob::glob_with(&pat, opts)? { + let file = file?; + let s = file.to_str().unwrap(); + if s == "." + || s == ".." + || s.ends_with("/.") + || s.ends_with("/..") + { + continue; + } + found = true; + expanded_words.push(s.to_string()); + } + if !found { + anyhow::bail!("no matches for {}", s); + } + } else { + expanded_words.push(s); + } + } + Ok(expanded_words) + } + + fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self { + assert!(matches!( + pair.as_rule(), + Rule::word | Rule::alternation_word + )); + Self { + parts: pair.into_inner().flat_map(WordPart::build_ast).collect(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum WordPart { + Alternation(Vec<Word>), + Substitution(String), + Var(String), + Bareword(String), + DoubleQuoted(String), + SingleQuoted(String), +} + +impl WordPart { + async fn eval(self, env: &Env) -> String { + match self { + Self::Alternation(_) => unreachable!(), + Self::Substitution(commands) => { + let mut cmd = tokio::process::Command::new( + crate::info::current_exe().unwrap(), + ); + cmd.args(&["-c", &commands]); + cmd.stdin(std::process::Stdio::inherit()); + cmd.stderr(std::process::Stdio::inherit()); + let mut out = + String::from_utf8(cmd.output().await.unwrap().stdout) + .unwrap(); + if out.ends_with('\n') { + out.truncate(out.len() - 1); + } + out + } + Self::Var(name) => { + env.var(&name).unwrap_or_else(|| "".to_string()) + } + Self::Bareword(s) + | Self::DoubleQuoted(s) + | Self::SingleQuoted(s) => s, + } + } + + fn build_ast( + pair: pest::iterators::Pair<Rule>, + ) -> impl Iterator<Item = Self> + '_ { + assert!(matches!( + pair.as_rule(), + Rule::word_part | Rule::alternation_word_part + )); + pair.into_inner().map(|pair| match pair.as_rule() { + Rule::substitution => { + let commands = pair.into_inner().next().unwrap(); + assert!(matches!(commands.as_rule(), Rule::commands)); + Self::Substitution(commands.as_str().to_string()) + } + Rule::var => { + let s = pair.as_str(); + let inner = s.strip_prefix('$').unwrap(); + Self::Var( + inner + .strip_prefix('{') + .map_or(inner, |inner| { + inner.strip_suffix('}').unwrap() + }) + .to_string(), + ) + } + Rule::bareword | Rule::alternation_bareword => { + Self::Bareword(strip_escape(pair.as_str())) + } + Rule::double_string => { + Self::DoubleQuoted(strip_escape(pair.as_str())) + } + Rule::single_string => { + Self::SingleQuoted(strip_basic_escape(pair.as_str())) + } + Rule::alternation => Self::Alternation( + pair.into_inner().map(Word::build_ast).collect(), + ), + _ => unreachable!(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct Redirect { + from: std::os::unix::io::RawFd, + to: Word, + dir: super::Direction, +} + +impl Redirect { + fn build_ast(pair: pest::iterators::Pair<Rule>) -> Self { + assert!(matches!(pair.as_rule(), Rule::redirect)); + let mut iter = pair.into_inner(); + + let prefix = iter.next().unwrap().as_str(); + let (from, dir) = prefix.strip_suffix(">>").map_or_else( + || { + prefix.strip_suffix('>').map_or_else( + || { + ( + prefix.strip_suffix('<').unwrap(), + super::Direction::In, + ) + }, + |from| (from, super::Direction::Out), + ) + }, + |from| (from, super::Direction::Append), + ); + let from = if from.is_empty() { + match dir { + super::Direction::In => 0, + super::Direction::Out | super::Direction::Append => 1, + } + } else { + parse_fd(from) + }; + + let to = Word::build_ast(iter.next().unwrap()); + + Self { from, to, dir } + } + + async fn eval(self, env: &Env) -> Result<super::Redirect> { + let to = if self.to.parts.len() == 1 { + if let WordPart::Bareword(s) = &self.to.parts[0] { + if let Some(fd) = s.strip_prefix('&') { + super::RedirectTarget::Fd(parse_fd(fd)) + } else { + let to = self.to.eval(env).await?; + assert_eq!(to.len(), 1); // TODO + let to = &to[0]; + super::RedirectTarget::File(std::path::PathBuf::from(to)) + } + } else { + let to = self.to.eval(env).await?; + assert_eq!(to.len(), 1); // TODO + let to = &to[0]; + super::RedirectTarget::File(std::path::PathBuf::from(to)) + } + } else { + let to = self.to.eval(env).await?; + assert_eq!(to.len(), 1); // TODO + let to = &to[0]; + super::RedirectTarget::File(std::path::PathBuf::from(to)) + }; + Ok(super::Redirect { + from: self.from, + to, + dir: self.dir, + }) + } +} + +fn strip_escape(s: &str) -> String { + let mut new = String::new(); + let mut escape = false; + for c in s.chars() { + if escape { + new.push(c); + escape = false; + } else { + match c { + '\\' => escape = true, + _ => new.push(c), + } + } + } + new +} + +fn strip_basic_escape(s: &str) -> String { + let mut new = String::new(); + let mut escape = false; + for c in s.chars() { + if escape { + match c { + '\\' | '\'' => {} + _ => new.push('\\'), + } + new.push(c); + escape = false; + } else { + match c { + '\\' => escape = true, + _ => new.push(c), + } + } + } + new +} + +fn parse_fd(s: &str) -> std::os::unix::io::RawFd { + match s { + "in" => 0, + "out" => 1, + "err" => 2, + _ => s.parse().unwrap(), + } +} + +fn expand_home(dir: &str) -> Result<String> { + if dir.starts_with('~') { + let path: std::path::PathBuf = dir.into(); + if let std::path::Component::Normal(prefix) = + path.components().next().unwrap() + { + let prefix_bytes = prefix.as_bytes(); + let name = if prefix_bytes == b"~" { + None + } else { + Some(std::ffi::OsStr::from_bytes(&prefix_bytes[1..])) + }; + if let Some(home) = home(name) { + Ok(home + .join(path.strip_prefix(prefix).unwrap()) + .to_str() + .unwrap() + .to_string()) + } else { + anyhow::bail!( + "no such user: {}", + name.map(std::ffi::OsStr::to_string_lossy) + .as_ref() + .unwrap_or(&std::borrow::Cow::Borrowed("(deleted)")) + ); + } + } else { + unreachable!() + } + } else { + Ok(dir.to_string()) + } +} + +fn home(user: Option<&std::ffi::OsStr>) -> Option<std::path::PathBuf> { + let user = user.map_or_else( + || users::get_user_by_uid(users::get_current_uid()), + users::get_user_by_name, + ); + user.map(|user| user.home_dir().to_path_buf()) +} + +#[cfg(test)] +#[path = "test_ast.rs"] +mod test; diff --git a/src/parse/mod.rs b/src/parse/mod.rs new file mode 100644 index 0000000..e2b7ec0 --- /dev/null +++ b/src/parse/mod.rs @@ -0,0 +1,169 @@ +pub mod ast; + +#[derive(Debug, Eq, PartialEq)] +pub struct Pipeline { + exes: Vec<Exe>, +} + +impl Pipeline { + pub fn into_exes(self) -> impl Iterator<Item = Exe> { + self.exes.into_iter() + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Exe { + exe: std::path::PathBuf, + args: Vec<String>, + redirects: Vec<Redirect>, +} + +impl Exe { + pub fn exe(&self) -> &std::path::Path { + &self.exe + } + + pub fn args(&self) -> &[String] { + &self.args + } + + pub fn append(&mut self, other: Self) { + let Self { + exe: _exe, + args, + redirects, + } = other; + self.args.extend(args); + self.redirects.extend(redirects); + } + + pub fn redirects(&self) -> &[Redirect] { + &self.redirects + } + + pub fn shift(&mut self) { + self.exe = std::path::PathBuf::from(self.args.remove(0)); + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Redirect { + pub from: std::os::unix::io::RawFd, + pub to: RedirectTarget, + pub dir: Direction, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum RedirectTarget { + Fd(std::os::unix::io::RawFd), + File(std::path::PathBuf), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + In, + Out, + Append, +} + +impl Direction { + pub fn open( + self, + path: &std::path::Path, + ) -> nix::Result<std::os::unix::io::RawFd> { + use nix::fcntl::OFlag; + use nix::sys::stat::Mode; + Ok(match self { + Self::In => nix::fcntl::open( + path, + OFlag::O_NOCTTY | OFlag::O_RDONLY, + Mode::empty(), + )?, + Self::Out => nix::fcntl::open( + path, + OFlag::O_CREAT + | OFlag::O_NOCTTY + | OFlag::O_WRONLY + | OFlag::O_TRUNC, + Mode::S_IRUSR + | Mode::S_IWUSR + | Mode::S_IRGRP + | Mode::S_IWGRP + | Mode::S_IROTH + | Mode::S_IWOTH, + )?, + Self::Append => nix::fcntl::open( + path, + OFlag::O_APPEND + | OFlag::O_CREAT + | OFlag::O_NOCTTY + | OFlag::O_WRONLY, + Mode::S_IRUSR + | Mode::S_IWUSR + | Mode::S_IRGRP + | Mode::S_IWGRP + | Mode::S_IROTH + | Mode::S_IWOTH, + )?, + }) + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct Error { + input: String, + e: pest::error::Error<ast::Rule>, +} + +impl Error { + fn new(input: String, e: pest::error::Error<ast::Rule>) -> Self { + Self { input, e } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.e.variant { + pest::error::ErrorVariant::ParsingError { + positives, + negatives, + } => { + if !positives.is_empty() { + write!(f, "expected {:?}", positives[0])?; + for rule in &positives[1..] { + write!(f, ", {:?}", rule)?; + } + if !negatives.is_empty() { + write!(f, "; ")?; + } + } + if !negatives.is_empty() { + write!(f, "unexpected {:?}", negatives[0])?; + for rule in &negatives[1..] { + write!(f, ", {:?}", rule)?; + } + } + writeln!(f)?; + writeln!(f, "{}", self.input)?; + match &self.e.location { + pest::error::InputLocation::Pos(i) => { + write!(f, "{}^", " ".repeat(*i))?; + } + pest::error::InputLocation::Span((i, j)) => { + write!(f, "{}{}", " ".repeat(*i), "^".repeat(j - i))?; + } + } + } + pest::error::ErrorVariant::CustomError { message } => { + write!(f, "{}", message)?; + } + } + Ok(()) + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.e) + } +} diff --git a/src/parse/test_ast.rs b/src/parse/test_ast.rs new file mode 100644 index 0000000..a1f83dd --- /dev/null +++ b/src/parse/test_ast.rs @@ -0,0 +1,507 @@ +use super::*; + +impl From<Pipeline> for Command { + fn from(pipeline: Pipeline) -> Self { + Self::Pipeline(pipeline) + } +} + +macro_rules! cs { + ($($commands:expr),*) => { + Commands { + commands: [$($commands),*] + .into_iter() + .map(|c| c.into()) + .collect(), + } + }; +} + +macro_rules! p { + ($span:expr, $($exes:expr),*) => { + Pipeline { + exes: vec![$($exes),*], + span: $span, + } + }; +} + +macro_rules! ep { + ($($exes:expr),*) => { + super::super::Pipeline { + exes: vec![$($exes),*], + } + }; +} + +macro_rules! e { + ($word:expr) => { + Exe { + exe: $word, + args: vec![], + redirects: vec![], + } + }; + ($word:expr, $($args:expr),*) => { + Exe { + exe: $word, + args: vec![$($args),*], + redirects: vec![], + } + }; + ($word:expr ; $($redirects:expr),*) => { + Exe { + exe: $word, + args: vec![], + redirects: vec![$($redirects),*], + } + }; + ($word:expr, $($args:expr),* ; $($redirects:expr),*) => { + Exe { + exe: $word, + args: vec![$($args),*], + redirects: vec![$($redirects),*], + } + }; +} + +macro_rules! ee { + ($exe:expr) => { + super::super::Exe { + exe: std::path::PathBuf::from($exe.to_string()), + args: vec![], + redirects: vec![], + } + }; + ($exe:expr, $($args:expr),*) => { + super::super::Exe { + exe: std::path::PathBuf::from($exe.to_string()), + args: [$($args),*] + .into_iter() + .map(|s| s.to_string()) + .collect(), + redirects: vec![], + } + }; +} + +macro_rules! r { + ($from:literal, $to:expr, $dir:ident) => { + Redirect { + from: $from, + to: $to, + dir: super::super::Direction::$dir, + } + }; +} + +macro_rules! w { + ($word:literal) => { + Word { + parts: vec![WordPart::Bareword($word.to_string())], + } + }; + ($($word:expr),*) => { + Word { + parts: vec![$($word),*], + } + } +} + +macro_rules! wpa { + ($($word:expr),*) => { + WordPart::Alternation(vec![$($word),*]) + } +} + +macro_rules! wpv { + ($var:literal) => { + WordPart::Var($var.to_string()) + }; +} + +macro_rules! wpb { + ($bareword:expr) => { + WordPart::Bareword($bareword.to_string()) + }; +} + +macro_rules! wpd { + ($doublequoted:expr) => { + WordPart::DoubleQuoted($doublequoted.to_string()) + }; +} + +macro_rules! wps { + ($singlequoted:expr) => { + WordPart::SingleQuoted($singlequoted.to_string()) + }; +} + +macro_rules! parse_eq { + ($line:literal, $parsed:expr) => { + assert_eq!(&Commands::parse($line).unwrap(), &$parsed) + }; +} + +macro_rules! eval_eq { + ($line:literal, $env:expr, $($evaled:expr),*) => {{ + let ast = Commands::parse($line).unwrap(); + let mut expected: Vec<super::super::Pipeline> + = vec![$($evaled),*]; + for command in ast.commands { + let pipeline = match command { + Command::Pipeline(p) + | Command::If(p) + | Command::While(p) => p, + _ => continue, + }; + assert_eq!( + pipeline.eval(&$env).await.unwrap(), + expected.remove(0) + ); + } + }}; +} + +macro_rules! deserialize_eq { + ($line:literal, $parsed:expr) => {{ + use serde::de::IntoDeserializer as _; + use serde::Deserialize as _; + let exe: Result<_, serde::de::value::Error> = + Exe::deserialize($line.into_deserializer()); + assert_eq!(exe.unwrap(), $parsed); + }}; +} + +macro_rules! eval_fails { + ($line:literal, $env:expr) => {{ + let ast = Commands::parse($line).unwrap(); + let mut fail = false; + for command in ast.commands { + let pipeline = match command { + Command::Pipeline(p) | Command::If(p) | Command::While(p) => { + p + } + _ => continue, + }; + if pipeline.eval(&$env).await.is_err() { + fail = true; + } + } + assert!(fail) + }}; +} + +#[test] +fn test_basic() { + parse_eq!("foo", cs!(p!((0, 3), e!(w!("foo"))))); + parse_eq!("foo bar", cs!(p!((0, 7), e!(w!("foo"), w!("bar"))))); + parse_eq!( + "foo bar baz", + cs!(p!((0, 11), e!(w!("foo"), w!("bar"), w!("baz")))) + ); + parse_eq!("foo | bar", cs!(p!((0, 9), e!(w!("foo")), e!(w!("bar"))))); + parse_eq!( + "command ls; perl -E 'say foo' | tr a-z A-Z; builtin echo bar", + cs!( + p!((0, 10), e!(w!("command"), w!("ls"))), + p!( + (12, 42), + e!(w!("perl"), w!("-E"), w!(wps!("say foo"))), + e!(w!("tr"), w!("a-z"), w!("A-Z")) + ), + p!((44, 60), e!(w!("builtin"), w!("echo"), w!("bar"))) + ) + ); + + // XXX this parse may change in the future + let exe = crate::info::current_exe() + .unwrap() + .into_os_string() + .into_string() + .unwrap(); + parse_eq!( + "seq 1 5 | (while read line; echo \"line: $line\"; end)", + cs!(p!( + (0, 52), + e!(w!("seq"), w!("1"), w!("5")), + e!( + w!(wps!(exe)), + w!(wps!("-c")), + w!(wps!("while read line; echo \"line: $line\"; end")) + ) + )) + ); + + parse_eq!("foo ''", cs!(p!((0, 6), e!(w!("foo"), w!())))); + parse_eq!("foo \"\"", cs!(p!((0, 6), e!(w!("foo"), w!())))); +} + +#[test] +fn test_whitespace() { + parse_eq!(" foo ", cs!(p!((3, 6), e!(w!("foo"))))); + parse_eq!( + " foo # this is a comment", + cs!(p!((3, 6), e!(w!("foo")))) + ); + parse_eq!("foo#comment", cs!(p!((0, 3), e!(w!("foo"))))); + parse_eq!( + "foo;bar|baz;quux#comment", + cs!( + p!((0, 3), e!(w!("foo"))), + p!((4, 11), e!(w!("bar")), e!(w!("baz"))), + p!((12, 16), e!(w!("quux"))) + ) + ); + parse_eq!( + "foo | bar ", + cs!(p!((0, 12), e!(w!("foo")), e!(w!("bar")))) + ); + parse_eq!( + " abc def ghi |jkl mno| pqr stu; vwxyz # comment", + cs!( + p!( + (2, 36), + e!(w!("abc"), w!("def"), w!("ghi")), + e!(w!("jkl"), w!("mno")), + e!(w!("pqr"), w!("stu")) + ), + p!((38, 43), e!(w!("vwxyz"))) + ) + ); + parse_eq!( + "foo 'bar # baz' \"quux # not a comment\" # comment", + cs!(p!( + (0, 38), + e!( + w!("foo"), + w!(wps!("bar # baz")), + w!(wpd!("quux # not a comment")) + ) + )) + ); +} + +#[test] +fn test_redirect() { + parse_eq!( + "foo > bar", + cs!(p!((0, 9), e!(w!("foo") ; r!(1, w!("bar"), Out)))) + ); + parse_eq!( + "foo <bar", + cs!(p!((0, 8), e!(w!("foo") ; r!(0, w!("bar"), In)))) + ); + parse_eq!( + "foo > /dev/null 2>&1", + cs!(p!( + (0, 20), + e!( + w!("foo") ; + r!(1, w!("/dev/null"), Out), r!(2, w!("&1"), Out) + ) + )) + ); + parse_eq!( + "foo >>bar", + cs!(p!((0, 9), e!(w!("foo") ; r!(1, w!("bar"), Append)))) + ); + parse_eq!( + "foo >> bar", + cs!(p!((0, 10), e!(w!("foo") ; r!(1, w!("bar"), Append)))) + ); + parse_eq!( + "foo > 'bar baz'", + cs!(p!((0, 15), e!(w!("foo") ; r!(1, w!(wps!("bar baz")), Out)))) + ); +} + +#[test] +fn test_escape() { + parse_eq!("foo\\ bar", cs!(p!((0, 8), e!(w!("foo bar"))))); + parse_eq!("'foo\\ bar'", cs!(p!((0, 10), e!(w!(wps!("foo\\ bar")))))); + parse_eq!("\"foo\\ bar\"", cs!(p!((0, 10), e!(w!(wpd!("foo bar")))))); + parse_eq!("\"foo\\\"bar\"", cs!(p!((0, 10), e!(w!(wpd!("foo\"bar")))))); + parse_eq!( + "'foo\\'bar\\\\'", + cs!(p!((0, 12), e!(w!(wps!("foo'bar\\"))))) + ); + parse_eq!( + "foo > bar\\ baz", + cs!(p!((0, 14), e!(w!("foo") ; r!(1, w!("bar baz"), Out)))) + ); +} + +#[test] +fn test_parts() { + parse_eq!( + "echo \"$HOME/bin\"", + cs!(p!((0, 16), e!(w!("echo"), w!(wpv!("HOME"), wpd!("/bin"))))) + ); + parse_eq!( + "echo \"dir: $HOME/bin\"", + cs!(p!( + (0, 21), + e!(w!("echo"), w!(wpd!("dir: "), wpv!("HOME"), wpd!("/bin"))) + )) + ); + parse_eq!( + "echo $HOME/bin", + cs!(p!((0, 14), e!(w!("echo"), w!(wpv!("HOME"), wpb!("/bin"))))) + ); + parse_eq!( + "echo '$HOME/bin'", + cs!(p!((0, 16), e!(w!("echo"), w!(wps!("$HOME/bin"))))) + ); + parse_eq!( + "echo \"foo\"\"bar\"", + cs!(p!((0, 15), e!(w!("echo"), w!(wpd!("foo"), wpd!("bar"))))) + ); + parse_eq!( + "echo $foo$bar$baz", + cs!(p!( + (0, 17), + e!(w!("echo"), w!(wpv!("foo"), wpv!("bar"), wpv!("baz"))) + )) + ); + parse_eq!( + "perl -E'say \"foo\"'", + cs!(p!( + (0, 18), + e!(w!("perl"), w!(wpb!("-E"), wps!("say \"foo\""))) + )) + ); +} + +#[test] +fn test_alternation() { + parse_eq!( + "echo {foo,bar}", + cs!(p!((0, 14), e!(w!("echo"), w!(wpa!(w!("foo"), w!("bar")))))) + ); + parse_eq!( + "echo {foo,bar}.rs", + cs!(p!( + (0, 17), + e!(w!("echo"), w!(wpa!(w!("foo"), w!("bar")), wpb!(".rs"))) + )) + ); + parse_eq!( + "echo {foo,bar,baz}.rs", + cs!(p!( + (0, 21), + e!( + w!("echo"), + w!(wpa!(w!("foo"), w!("bar"), w!("baz")), wpb!(".rs")) + ) + )) + ); + parse_eq!( + "echo {foo,}.rs", + cs!(p!( + (0, 14), + e!(w!("echo"), w!(wpa!(w!("foo"), w!()), wpb!(".rs"))) + )) + ); + parse_eq!( + "echo {foo}", + cs!(p!((0, 10), e!(w!("echo"), w!(wpa!(w!("foo")))))) + ); + parse_eq!("echo {}", cs!(p!((0, 7), e!(w!("echo"), w!(wpa!(w!())))))); + parse_eq!( + "echo {foo,bar}.{rs,c}", + cs!(p!( + (0, 21), + e!( + w!("echo"), + w!( + wpa!(w!("foo"), w!("bar")), + wpb!("."), + wpa!(w!("rs"), w!("c")) + ) + ) + )) + ); + parse_eq!( + "echo {$foo,\"${HOME}/bin\"}.{'r'\"s\",c}", + cs!(p!( + (0, 36), + e!( + w!("echo"), + w!( + wpa!(w!(wpv!("foo")), w!(wpv!("HOME"), wpd!("/bin"))), + wpb!("."), + wpa!(w!(wps!("r"), wpd!("s")), w!("c")) + ) + ) + )) + ); +} + +#[tokio::main] +#[test] +async fn test_eval_alternation() { + let mut env = Env::new().unwrap(); + env.set_var("HOME", "/home/test"); + env.set_var("foo", "value-of-foo"); + + eval_eq!("echo {foo,bar}", env, ep!(ee!("echo", "foo", "bar"))); + eval_eq!( + "echo {foo,bar}.rs", + env, + ep!(ee!("echo", "foo.rs", "bar.rs")) + ); + eval_eq!( + "echo {foo,bar,baz}.rs", + env, + ep!(ee!("echo", "foo.rs", "bar.rs", "baz.rs")) + ); + eval_eq!("echo {foo,}.rs", env, ep!(ee!("echo", "foo.rs", ".rs"))); + eval_eq!("echo {foo}", env, ep!(ee!("echo", "foo"))); + eval_eq!("echo {}", env, ep!(ee!("echo", ""))); + eval_eq!( + "echo {foo,bar}.{rs,c}", + env, + ep!(ee!("echo", "foo.rs", "foo.c", "bar.rs", "bar.c")) + ); + eval_eq!( + "echo {$foo,\"${HOME}/bin\"}.{'r'\"s\",c}", + env, + ep!(ee!( + "echo", + "value-of-foo.rs", + "value-of-foo.c", + "/home/test/bin.rs", + "/home/test/bin.c" + )) + ); +} + +#[tokio::main] +#[test] +async fn test_eval_glob() { + let env = Env::new().unwrap(); + + eval_eq!( + "echo *.toml", + env, + ep!(ee!("echo", "Cargo.toml", "deny.toml")) + ); + eval_eq!("echo .*.toml", env, ep!(ee!("echo", ".rustfmt.toml"))); + eval_eq!( + "echo *.{lock,toml}", + env, + ep!(ee!("echo", "Cargo.lock", "Cargo.toml", "deny.toml")) + ); + eval_eq!("echo foo]", env, ep!(ee!("echo", "foo]"))); + eval_fails!("echo foo[", env); + eval_fails!("echo *.doesnotexist", env); + eval_fails!("echo *.{toml,doesnotexist}", env); +} + +#[test] +fn test_deserialize() { + deserialize_eq!("foo", e!(w!("foo"))); + deserialize_eq!("foo bar baz", e!(w!("foo"), w!("bar"), w!("baz"))); +} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..bc48955 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,51 @@ +pub use crate::env::Env; +pub use anyhow::{anyhow, Result}; + +pub use std::io::{Read as _, Write as _}; + +pub use futures_util::future::FutureExt as _; +pub use futures_util::stream::StreamExt as _; +pub use futures_util::stream::TryStreamExt as _; +pub use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + +pub use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _}; +pub use std::os::unix::io::{AsRawFd as _, FromRawFd as _, IntoRawFd as _}; +pub use std::os::unix::process::ExitStatusExt as _; +pub use users::os::unix::UserExt as _; + +pub use ext::Result as _; + +mod ext { + pub trait Result { + type T; + type E; + + fn allow(self, allow_e: Self::E) -> Self; + fn allow_with(self, allow_e: Self::E, default_t: Self::T) -> Self; + } + + impl<T, E> Result for std::result::Result<T, E> + where + T: std::default::Default, + E: std::cmp::PartialEq, + { + type T = T; + type E = E; + + fn allow(self, allow_e: Self::E) -> Self { + self.or_else(|e| { + if e == allow_e { + Ok(std::default::Default::default()) + } else { + Err(e) + } + }) + } + + fn allow_with(self, allow_e: Self::E, default_t: Self::T) -> Self { + self.or_else( + |e| if e == allow_e { Ok(default_t) } else { Err(e) }, + ) + } + } +} diff --git a/src/readline.rs b/src/readline.rs deleted file mode 100644 index 8b3c847..0000000 --- a/src/readline.rs +++ /dev/null @@ -1,198 +0,0 @@ -use textmode::Textmode as _; -use unicode_width::{UnicodeWidthChar as _, UnicodeWidthStr as _}; - -pub struct Readline { - size: (u16, u16), - prompt: String, - input_line: String, - pos: usize, -} - -impl Readline { - pub fn new() -> Self { - Self { - size: (24, 80), - prompt: if users::get_current_uid() == 0 { - "# " - } else { - "$ " - } - .into(), - input_line: "".into(), - pos: 0, - } - } - - pub async fn handle_key( - &mut self, - key: textmode::Key, - ) -> Option<crate::action::Action> { - match key { - textmode::Key::String(s) => self.add_input(&s), - textmode::Key::Char(c) => { - self.add_input(&c.to_string()); - } - textmode::Key::Ctrl(b'c') => self.clear_input(), - textmode::Key::Ctrl(b'd') => { - return Some(crate::action::Action::Quit); - } - textmode::Key::Ctrl(b'l') => { - return Some(crate::action::Action::ForceRedraw); - } - textmode::Key::Ctrl(b'm') => { - let cmd = self.input(); - self.clear_input(); - return Some(crate::action::Action::Run(cmd)); - } - textmode::Key::Ctrl(b'u') => self.clear_backwards(), - textmode::Key::Backspace => self.backspace(), - textmode::Key::Left => self.cursor_left(), - textmode::Key::Right => self.cursor_right(), - _ => {} - } - Some(crate::action::Action::Render) - } - - pub async fn render( - &self, - out: &mut textmode::Output, - focus: bool, - ) -> anyhow::Result<()> { - let mut pwd = std::env::current_dir()?.display().to_string(); - let home = std::env::var("HOME")?; - if pwd.starts_with(&home) { - pwd.replace_range(..home.len(), "~"); - } - let user = users::get_current_username() - .unwrap() - .to_string_lossy() - .into_owned(); - let mut hostname = - hostname::get().unwrap().to_string_lossy().into_owned(); - if let Some(idx) = hostname.find('.') { - hostname.truncate(idx); - } - let id = format!("{}@{}", user, hostname); - let idlen: u16 = id.len().try_into().unwrap(); - let time = chrono::Local::now().format("%H:%M:%S").to_string(); - let timelen: u16 = time.len().try_into().unwrap(); - - out.move_to(self.size.0 - 2, 0); - out.set_bgcolor(textmode::Color::Rgb(32, 32, 64)); - out.write(b"\x1b[K"); - out.write(b" ("); - out.write_str(&pwd); - out.write(b")"); - out.move_to(self.size.0 - 2, self.size.1 - 4 - idlen - timelen); - out.write_str(&id); - out.write_str(" ["); - out.write_str(&time); - out.write_str("]"); - - out.move_to(self.size.0 - 1, 0); - if focus { - out.set_fgcolor(textmode::color::BLACK); - out.set_bgcolor(textmode::color::CYAN); - } else { - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - } - out.write_str(&self.prompt); - out.reset_attributes(); - out.set_bgcolor(textmode::Color::Rgb(32, 32, 32)); - out.write(b"\x1b[K"); - out.write_str(&self.input_line); - out.reset_attributes(); - out.move_to(self.size.0 - 1, self.prompt_width() + self.pos_width()); - if focus { - out.write(b"\x1b[?25h"); - } - Ok(()) - } - - pub async fn resize(&mut self, size: (u16, u16)) { - self.size = size; - } - - pub fn lines(&self) -> usize { - 2 // XXX handle wrapping - } - - fn input(&self) -> String { - self.input_line.clone() - } - - fn add_input(&mut self, s: &str) { - self.input_line.insert_str(self.byte_pos(), s); - self.pos += s.chars().count(); - } - - fn backspace(&mut self) { - while self.pos > 0 { - self.pos -= 1; - let width = - self.input_line.remove(self.byte_pos()).width().unwrap_or(0); - if width > 0 { - break; - } - } - } - - fn clear_input(&mut self) { - self.input_line.clear(); - self.pos = 0; - } - - fn clear_backwards(&mut self) { - self.input_line = self.input_line.chars().skip(self.pos).collect(); - self.pos = 0; - } - - fn cursor_left(&mut self) { - if self.pos == 0 { - return; - } - self.pos -= 1; - while let Some(c) = self.input_line.chars().nth(self.pos) { - if c.width().unwrap_or(0) == 0 { - self.pos -= 1; - } else { - break; - } - } - } - - fn cursor_right(&mut self) { - if self.pos == self.input_line.chars().count() { - return; - } - self.pos += 1; - while let Some(c) = self.input_line.chars().nth(self.pos) { - if c.width().unwrap_or(0) == 0 { - self.pos += 1; - } else { - break; - } - } - } - - fn prompt_width(&self) -> u16 { - self.prompt.width().try_into().unwrap() - } - - fn pos_width(&self) -> u16 { - self.input_line - .chars() - .take(self.pos) - .collect::<String>() - .width() - .try_into() - .unwrap() - } - - fn byte_pos(&self) -> usize { - self.input_line - .char_indices() - .nth(self.pos) - .map_or(self.input_line.len(), |(i, _)| i) - } -} diff --git a/src/runner/builtins/command.rs b/src/runner/builtins/command.rs new file mode 100644 index 0000000..16d8b40 --- /dev/null +++ b/src/runner/builtins/command.rs @@ -0,0 +1,373 @@ +use crate::runner::prelude::*; + +pub struct Command { + exe: crate::parse::Exe, + f: super::Builtin, + cfg: Cfg, +} + +impl Command { + pub fn new( + exe: crate::parse::Exe, + io: Io, + ) -> Result<Self, crate::parse::Exe> { + if let Some(s) = exe.exe().to_str() { + if let Some(f) = super::BUILTINS.get(s) { + Ok(Self { + exe, + f, + cfg: Cfg::new(io), + }) + } else { + Err(exe) + } + } else { + Err(exe) + } + } + + pub fn stdin(&mut self, fh: std::fs::File) { + self.cfg.io.set_stdin(fh); + } + + pub fn stdout(&mut self, fh: std::fs::File) { + self.cfg.io.set_stdout(fh); + } + + pub fn stderr(&mut self, fh: std::fs::File) { + self.cfg.io.set_stderr(fh); + } + + // Safety: see pre_exec in tokio::process::Command (this is just a + // wrapper) + pub unsafe fn pre_exec<F>(&mut self, f: F) + where + F: 'static + FnMut() -> std::io::Result<()> + Send + Sync, + { + self.cfg.pre_exec(f); + } + + pub fn apply_redirects(&mut self, redirects: &[crate::parse::Redirect]) { + self.cfg.io.apply_redirects(redirects); + } + + pub fn spawn(self, env: &Env) -> Result<Child> { + let Self { f, exe, cfg } = self; + (f)(exe, env, cfg) + } +} + +pub struct Cfg { + io: Io, + pre_exec: Option< + Box<dyn 'static + FnMut() -> std::io::Result<()> + Send + Sync>, + >, +} + +impl Cfg { + fn new(io: Io) -> Self { + Self { io, pre_exec: None } + } + + pub fn io(&self) -> &Io { + &self.io + } + + // Safety: see pre_exec in tokio::process::Command (this is just a + // wrapper) + pub unsafe fn pre_exec<F>(&mut self, f: F) + where + F: 'static + FnMut() -> std::io::Result<()> + Send + Sync, + { + self.pre_exec = Some(Box::new(f)); + } + + pub fn setup_command(mut self, cmd: &mut crate::runner::Command) { + self.io.setup_command(cmd); + if let Some(pre_exec) = self.pre_exec.take() { + // Safety: pre_exec can only have been set by calling the pre_exec + // method, which is itself unsafe, so the safety comments at the + // point where that is called are the relevant ones + unsafe { cmd.pre_exec(pre_exec) }; + } + } +} + +#[derive(Debug, Clone)] +pub struct Io { + fds: std::collections::HashMap< + std::os::unix::io::RawFd, + std::sync::Arc<File>, + >, +} + +impl Io { + pub fn new() -> Self { + Self { + fds: std::collections::HashMap::new(), + } + } + + fn stdin(&self) -> Option<std::sync::Arc<File>> { + self.fds.get(&0).map(std::sync::Arc::clone) + } + + pub fn set_stdin<T: std::os::unix::io::IntoRawFd>(&mut self, stdin: T) { + if let Some(file) = self.fds.remove(&0) { + File::maybe_drop(file); + } + self.fds.insert( + 0, + // Safety: we just acquired stdin via into_raw_fd, which acquires + // ownership of the fd, so we are now the sole owner + std::sync::Arc::new(unsafe { File::input(stdin.into_raw_fd()) }), + ); + } + + fn stdout(&self) -> Option<std::sync::Arc<File>> { + self.fds.get(&1).map(std::sync::Arc::clone) + } + + pub fn set_stdout<T: std::os::unix::io::IntoRawFd>(&mut self, stdout: T) { + if let Some(file) = self.fds.remove(&1) { + File::maybe_drop(file); + } + self.fds.insert( + 1, + // Safety: we just acquired stdout via into_raw_fd, which acquires + // ownership of the fd, so we are now the sole owner + std::sync::Arc::new(unsafe { + File::output(stdout.into_raw_fd()) + }), + ); + } + + fn stderr(&self) -> Option<std::sync::Arc<File>> { + self.fds.get(&2).map(std::sync::Arc::clone) + } + + pub fn set_stderr<T: std::os::unix::io::IntoRawFd>(&mut self, stderr: T) { + if let Some(file) = self.fds.remove(&2) { + File::maybe_drop(file); + } + self.fds.insert( + 2, + // Safety: we just acquired stderr via into_raw_fd, which acquires + // ownership of the fd, so we are now the sole owner + std::sync::Arc::new(unsafe { + File::output(stderr.into_raw_fd()) + }), + ); + } + + pub fn apply_redirects(&mut self, redirects: &[crate::parse::Redirect]) { + for redirect in redirects { + let to = match &redirect.to { + crate::parse::RedirectTarget::Fd(fd) => { + std::sync::Arc::clone(&self.fds[fd]) + } + crate::parse::RedirectTarget::File(path) => { + let fd = redirect.dir.open(path).unwrap(); + match redirect.dir { + crate::parse::Direction::In => { + // Safety: we just opened fd, and nothing else has + // or can use it + std::sync::Arc::new(unsafe { File::input(fd) }) + } + crate::parse::Direction::Out + | crate::parse::Direction::Append => { + // Safety: we just opened fd, and nothing else has + // or can use it + std::sync::Arc::new(unsafe { File::output(fd) }) + } + } + } + }; + self.fds.insert(redirect.from, to); + } + } + + pub fn read_line_stdin(&self) -> Result<(String, bool)> { + let mut line = vec![]; + if let Some(file) = self.stdin() { + if let File::In(fh) = &*file { + // we have to read only a single character at a time here + // because stdin needs to be shared across all commands in the + // command list, some of which may be builtins and others of + // which may be external commands - if we read past the end of + // a line, then the characters past the end of that line will + // no longer be available to the next command, since we have + // them buffered in memory rather than them being on the stdin + // pipe. + for byte in fh.bytes() { + let byte = byte?; + line.push(byte); + if byte == b'\n' { + break; + } + } + } + } + let done = line.is_empty(); + let mut line = String::from_utf8(line).unwrap(); + if line.ends_with('\n') { + line.truncate(line.len() - 1); + } + Ok((line, done)) + } + + pub fn write_stdout(&self, buf: &[u8]) -> Result<()> { + if let Some(file) = self.stdout() { + if let File::Out(fh) = &*file { + Ok((&*fh).write_all(buf)?) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + pub fn write_stderr(&self, buf: &[u8]) -> Result<()> { + if let Some(file) = self.stderr() { + if let File::Out(fh) = &*file { + Ok((&*fh).write_all(buf)?) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + pub fn setup_command(mut self, cmd: &mut crate::runner::Command) { + if let Some(stdin) = self.fds.remove(&0) { + if let Ok(stdin) = std::sync::Arc::try_unwrap(stdin) { + let stdin = stdin.into_raw_fd(); + if stdin != 0 { + // Safety: we just acquired stdin via into_raw_fd, which + // acquires ownership of the fd, so we are now the sole + // owner + cmd.stdin(unsafe { std::fs::File::from_raw_fd(stdin) }); + self.fds.remove(&0); + } + } + } + if let Some(stdout) = self.fds.remove(&1) { + if let Ok(stdout) = std::sync::Arc::try_unwrap(stdout) { + let stdout = stdout.into_raw_fd(); + if stdout != 1 { + // Safety: we just acquired stdout via into_raw_fd, which + // acquires ownership of the fd, so we are now the sole + // owner + cmd.stdout(unsafe { std::fs::File::from_raw_fd(stdout) }); + self.fds.remove(&1); + } + } + } + if let Some(stderr) = self.fds.remove(&2) { + if let Ok(stderr) = std::sync::Arc::try_unwrap(stderr) { + let stderr = stderr.into_raw_fd(); + if stderr != 2 { + // Safety: we just acquired stderr via into_raw_fd, which + // acquires ownership of the fd, so we are now the sole + // owner + cmd.stderr(unsafe { std::fs::File::from_raw_fd(stderr) }); + self.fds.remove(&2); + } + } + } + } +} + +impl Drop for Io { + fn drop(&mut self) { + for (_, file) in self.fds.drain() { + File::maybe_drop(file); + } + } +} + +#[derive(Debug)] +pub enum File { + In(std::fs::File), + Out(std::fs::File), +} + +impl File { + // Safety: fd must not be owned by any other File object + pub unsafe fn input(fd: std::os::unix::io::RawFd) -> Self { + Self::In(std::fs::File::from_raw_fd(fd)) + } + + // Safety: fd must not be owned by any other File object + pub unsafe fn output(fd: std::os::unix::io::RawFd) -> Self { + Self::Out(std::fs::File::from_raw_fd(fd)) + } + + fn maybe_drop(file: std::sync::Arc<Self>) { + if let Ok(file) = std::sync::Arc::try_unwrap(file) { + if file.as_raw_fd() <= 2 { + let _ = file.into_raw_fd(); + } + } + } +} + +impl std::os::unix::io::AsRawFd for File { + fn as_raw_fd(&self) -> std::os::unix::io::RawFd { + match self { + Self::In(fh) | Self::Out(fh) => fh.as_raw_fd(), + } + } +} + +impl std::os::unix::io::IntoRawFd for File { + fn into_raw_fd(self) -> std::os::unix::io::RawFd { + match self { + Self::In(fh) | Self::Out(fh) => fh.into_raw_fd(), + } + } +} + +pub enum Child { + Task(tokio::task::JoinHandle<std::process::ExitStatus>), + Wrapped(Box<crate::runner::Child>), +} + +impl Child { + pub fn new_task<F>(f: F) -> Self + where + F: FnOnce() -> std::process::ExitStatus + Send + 'static, + { + Self::Task(tokio::task::spawn_blocking(f)) + } + + pub fn new_wrapped(child: crate::runner::Child) -> Self { + Self::Wrapped(Box::new(child)) + } + + pub fn id(&self) -> Option<u32> { + match self { + Self::Task(_) => None, + Self::Wrapped(child) => child.id(), + } + } + + pub fn status( + self, + ) -> std::pin::Pin< + Box< + dyn std::future::Future<Output = Result<std::process::ExitStatus>> + + Send + + Sync, + >, + > { + Box::pin(async move { + match self { + Self::Task(task) => task.await.map_err(|e| anyhow!(e)), + Self::Wrapped(child) => child.status().await, + } + }) + } +} diff --git a/src/runner/builtins/mod.rs b/src/runner/builtins/mod.rs new file mode 100644 index 0000000..b714c58 --- /dev/null +++ b/src/runner/builtins/mod.rs @@ -0,0 +1,242 @@ +use crate::runner::prelude::*; + +pub mod command; +pub use command::{Child, Command, File, Io}; + +type Builtin = &'static (dyn for<'a> Fn( + crate::parse::Exe, + &'a Env, + command::Cfg, +) -> Result<command::Child> + + Sync + + Send); + +#[allow(clippy::as_conversions)] +static BUILTINS: once_cell::sync::Lazy< + std::collections::HashMap<&'static str, Builtin>, +> = once_cell::sync::Lazy::new(|| { + let mut builtins = std::collections::HashMap::new(); + builtins.insert("cd", &cd as Builtin); + builtins.insert("set", &set); + builtins.insert("unset", &unset); + builtins.insert("echo", &echo); + builtins.insert("read", &read); + builtins.insert("and", &and); + builtins.insert("or", &or); + builtins.insert("command", &command); + builtins.insert("builtin", &builtin); + builtins +}); + +macro_rules! bail { + ($cfg:expr, $exe:expr, $msg:expr $(,)?) => { + $cfg.io().write_stderr( + format!("{}: {}\n", $exe.exe().display(), $msg).as_bytes() + ) + .unwrap(); + return std::process::ExitStatus::from_raw(1 << 8); + }; + ($cfg:expr, $exe:expr, $msg:expr, $($arg:tt)*) => { + $cfg.io().write_stderr( + format!("{}: ", $exe.exe().display()).as_bytes() + ) + .unwrap(); + $cfg.io().write_stderr(format!($msg, $($arg)*).as_bytes()) + .unwrap(); + $cfg.io().write_stderr(b"\n").unwrap(); + return std::process::ExitStatus::from_raw(1 << 8); + }; +} + +// clippy can't tell that the type is necessary +#[allow(clippy::unnecessary_wraps)] +fn cd( + exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + let prev_pwd = env.prev_pwd(); + let home = env.var("HOME"); + Ok(command::Child::new_task(move || { + let dir = if let Some(dir) = exe.args().get(0) { + if dir.is_empty() { + ".".to_string().into() + } else if dir == "-" { + prev_pwd + } else { + dir.into() + } + } else { + let dir = home; + if let Some(dir) = dir { + dir.into() + } else { + bail!(cfg, exe, "could not find home directory"); + } + }; + if let Err(e) = std::env::set_current_dir(&dir) { + bail!( + cfg, + exe, + "{}: {}", + crate::format::io_error(&e), + dir.display() + ); + } + std::process::ExitStatus::from_raw(0) + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn set( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + Ok(command::Child::new_task(move || { + let k = if let Some(k) = exe.args().get(0).map(String::as_str) { + k + } else { + bail!(cfg, exe, "usage: set key value"); + }; + let v = if let Some(v) = exe.args().get(1).map(String::as_str) { + v + } else { + bail!(cfg, exe, "usage: set key value"); + }; + + std::env::set_var(k, v); + std::process::ExitStatus::from_raw(0) + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn unset( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + Ok(command::Child::new_task(move || { + let k = if let Some(k) = exe.args().get(0).map(String::as_str) { + k + } else { + bail!(cfg, exe, "usage: unset key"); + }; + + std::env::remove_var(k); + std::process::ExitStatus::from_raw(0) + })) +} + +// clippy can't tell that the type is necessary +#[allow(clippy::unnecessary_wraps)] +// mostly just for testing and ensuring that builtins work, i'll likely remove +// this later, since the binary seems totally fine +fn echo( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + Ok(command::Child::new_task(move || { + macro_rules! write_stdout { + ($bytes:expr) => { + if let Err(e) = cfg.io().write_stdout($bytes) { + cfg.io() + .write_stderr(format!("echo: {}", e).as_bytes()) + .unwrap(); + return std::process::ExitStatus::from_raw(1 << 8); + } + }; + } + let count = exe.args().len(); + for (i, arg) in exe.args().iter().enumerate() { + write_stdout!(arg.as_bytes()); + if i == count - 1 { + write_stdout!(b"\n"); + } else { + write_stdout!(b" "); + } + } + + std::process::ExitStatus::from_raw(0) + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn read( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + Ok(command::Child::new_task(move || { + let var = if let Some(var) = exe.args().get(0).map(String::as_str) { + var + } else { + bail!(cfg, exe, "usage: read var"); + }; + + let (val, done) = match cfg.io().read_line_stdin() { + Ok((line, done)) => (line, done), + Err(e) => { + bail!(cfg, exe, e); + } + }; + + std::env::set_var(var, val); + std::process::ExitStatus::from_raw(if done { 1 << 8 } else { 0 }) + })) +} + +fn and( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + exe.shift(); + if env.latest_status().success() { + let mut cmd = crate::runner::Command::new(exe, cfg.io().clone()); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) + } else { + let status = env.latest_status(); + Ok(command::Child::new_task(move || status)) + } +} + +fn or( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + exe.shift(); + if env.latest_status().success() { + let status = env.latest_status(); + Ok(command::Child::new_task(move || status)) + } else { + let mut cmd = crate::runner::Command::new(exe, cfg.io().clone()); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) + } +} + +fn command( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + exe.shift(); + let mut cmd = crate::runner::Command::new_binary(&exe); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) +} + +fn builtin( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + exe.shift(); + let mut cmd = crate::runner::Command::new_builtin(exe, cfg.io().clone()); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) +} diff --git a/src/runner/command.rs b/src/runner/command.rs new file mode 100644 index 0000000..cbc8dee --- /dev/null +++ b/src/runner/command.rs @@ -0,0 +1,203 @@ +use crate::runner::prelude::*; + +pub struct Command { + inner: Inner, + exe: std::path::PathBuf, + redirects: Vec<crate::parse::Redirect>, + pre_exec: Option< + Box<dyn FnMut() -> std::io::Result<()> + Send + Sync + 'static>, + >, +} + +impl Command { + pub fn new(exe: crate::parse::Exe, io: super::builtins::Io) -> Self { + let exe_path = exe.exe().to_path_buf(); + let redirects = exe.redirects().to_vec(); + Self { + inner: super::builtins::Command::new(exe, io).map_or_else( + |exe| Self::new_binary(&exe).inner, + Inner::Builtin, + ), + exe: exe_path, + redirects, + pre_exec: None, + } + } + + pub fn new_binary(exe: &crate::parse::Exe) -> Self { + let exe_path = exe.exe().to_path_buf(); + let redirects = exe.redirects().to_vec(); + let mut cmd = tokio::process::Command::new(exe.exe()); + cmd.args(exe.args()); + Self { + inner: Inner::Binary(cmd), + exe: exe_path, + redirects, + pre_exec: None, + } + } + + pub fn new_builtin( + exe: crate::parse::Exe, + io: super::builtins::Io, + ) -> Self { + let exe_path = exe.exe().to_path_buf(); + let redirects = exe.redirects().to_vec(); + Self { + inner: super::builtins::Command::new(exe, io) + .map_or_else(|_| todo!(), Inner::Builtin), + exe: exe_path, + redirects, + pre_exec: None, + } + } + + pub fn stdin(&mut self, fh: std::fs::File) { + match &mut self.inner { + Inner::Binary(cmd) => { + cmd.stdin(fh); + } + Inner::Builtin(cmd) => { + cmd.stdin(fh); + } + } + } + + pub fn stdout(&mut self, fh: std::fs::File) { + match &mut self.inner { + Inner::Binary(cmd) => { + cmd.stdout(fh); + } + Inner::Builtin(cmd) => { + cmd.stdout(fh); + } + } + } + + pub fn stderr(&mut self, fh: std::fs::File) { + match &mut self.inner { + Inner::Binary(cmd) => { + cmd.stderr(fh); + } + Inner::Builtin(cmd) => { + cmd.stderr(fh); + } + } + } + + // Safety: see pre_exec in tokio::process::Command (this is just a + // wrapper) + pub unsafe fn pre_exec<F>(&mut self, f: F) + where + F: 'static + FnMut() -> std::io::Result<()> + Send + Sync, + { + self.pre_exec = Some(Box::new(f)); + } + + pub fn spawn(self, env: &Env) -> Result<Child> { + let Self { + inner, + exe, + redirects, + pre_exec, + } = self; + + #[allow(clippy::as_conversions)] + let pre_exec = pre_exec.map_or_else( + || { + let redirects = redirects.clone(); + Box::new(move || { + apply_redirects(&redirects)?; + Ok(()) + }) + as Box<dyn FnMut() -> std::io::Result<()> + Send + Sync> + }, + |mut pre_exec| { + let redirects = redirects.clone(); + Box::new(move || { + apply_redirects(&redirects)?; + pre_exec()?; + Ok(()) + }) + }, + ); + match inner { + Inner::Binary(mut cmd) => { + // Safety: open, dup2, and close are async-signal-safe + // functions + unsafe { cmd.pre_exec(pre_exec) }; + Ok(Child::Binary(cmd.spawn().map_err(|e| { + anyhow!( + "{}: {}", + crate::format::io_error(&e), + exe.display() + ) + })?)) + } + Inner::Builtin(mut cmd) => { + // Safety: open, dup2, and close are async-signal-safe + // functions + unsafe { cmd.pre_exec(pre_exec) }; + cmd.apply_redirects(&redirects); + Ok(Child::Builtin(cmd.spawn(env)?)) + } + } + } +} + +pub enum Inner { + Binary(tokio::process::Command), + Builtin(super::builtins::Command), +} + +pub enum Child { + Binary(tokio::process::Child), + Builtin(super::builtins::Child), +} + +impl Child { + pub fn id(&self) -> Option<u32> { + match self { + Self::Binary(child) => child.id(), + Self::Builtin(child) => child.id(), + } + } + + pub fn status( + self, + ) -> std::pin::Pin< + Box< + dyn std::future::Future<Output = Result<std::process::ExitStatus>> + + Send + + Sync, + >, + > { + Box::pin(async move { + match self { + // this case is handled by waitpid + Self::Binary(_) => unreachable!(), + Self::Builtin(child) => Ok(child.status().await?), + } + }) + } +} + +fn apply_redirects( + redirects: &[crate::parse::Redirect], +) -> std::io::Result<()> { + for redirect in redirects { + match &redirect.to { + crate::parse::RedirectTarget::Fd(fd) => { + nix::unistd::dup2(*fd, redirect.from)?; + } + crate::parse::RedirectTarget::File(path) => { + let fd = redirect.dir.open(path)?; + if fd != redirect.from { + nix::unistd::dup2(fd, redirect.from)?; + nix::unistd::close(fd)?; + } + } + } + } + Ok(()) +} diff --git a/src/runner/mod.rs b/src/runner/mod.rs new file mode 100644 index 0000000..91e268a --- /dev/null +++ b/src/runner/mod.rs @@ -0,0 +1,499 @@ +use crate::runner::prelude::*; + +mod builtins; +mod command; +pub use command::{Child, Command}; +mod prelude; +mod sys; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum Event { + RunPipeline((usize, usize)), + Suspend, + Exit(Env), +} + +struct Stack { + frames: Vec<Frame>, +} + +impl Stack { + fn new() -> Self { + Self { frames: vec![] } + } + + fn push(&mut self, frame: Frame) { + self.frames.push(frame); + } + + fn pop(&mut self) -> Frame { + self.frames.pop().unwrap() + } + + fn top(&self) -> Option<&Frame> { + self.frames.last() + } + + fn top_mut(&mut self) -> Option<&mut Frame> { + self.frames.last_mut() + } + + fn current_pc(&self, pc: usize) -> bool { + match self.top() { + Some(Frame::If(..)) | None => false, + Some(Frame::While(_, start) | Frame::For(_, start, _)) => { + *start == pc + } + } + } + + fn should_execute(&self) -> bool { + for frame in &self.frames { + if matches!( + frame, + Frame::If(false, ..) + | Frame::While(false, ..) + | Frame::For(false, ..) + ) { + return false; + } + } + true + } +} + +enum Frame { + If(bool, bool), + While(bool, usize), + For(bool, usize, Vec<String>), +} + +pub async fn main( + commands: String, + shell_write: &mut Option<tokio::fs::File>, +) -> Result<i32> { + let mut env = Env::new_from_env()?; + let config = crate::config::Config::load()?; + run_commands(commands, &mut env, &config, shell_write).await?; + let status = env.latest_status(); + write_event(shell_write, Event::Exit(env)).await?; + + if let Some(signal) = status.signal() { + nix::sys::signal::raise(signal.try_into().unwrap())?; + } + Ok(status.code().unwrap()) +} + +async fn run_commands( + commands: String, + env: &mut Env, + config: &crate::config::Config, + shell_write: &mut Option<tokio::fs::File>, +) -> Result<()> { + let commands = crate::parse::ast::Commands::parse(&commands)?; + let commands = commands.commands(); + let mut pc = 0; + let mut stack = Stack::new(); + while pc < commands.len() { + match &commands[pc] { + crate::parse::ast::Command::Pipeline(pipeline) => { + if stack.should_execute() { + run_pipeline(pipeline.clone(), env, config, shell_write) + .await?; + } + pc += 1; + } + crate::parse::ast::Command::If(pipeline) => { + let should = stack.should_execute(); + if !stack.current_pc(pc) { + stack.push(Frame::If(false, false)); + } + if should { + let status = env.latest_status(); + run_pipeline(pipeline.clone(), env, config, shell_write) + .await?; + if let Some(Frame::If(should, found)) = stack.top_mut() { + *should = env.latest_status().success(); + if *should { + *found = true; + } + } else { + unreachable!(); + } + env.set_status(status); + } + pc += 1; + } + crate::parse::ast::Command::While(pipeline) => { + let should = stack.should_execute(); + if !stack.current_pc(pc) { + stack.push(Frame::While(false, pc)); + } + if should { + let status = env.latest_status(); + run_pipeline(pipeline.clone(), env, config, shell_write) + .await?; + if let Some(Frame::While(should, _)) = stack.top_mut() { + *should = env.latest_status().success(); + } else { + unreachable!(); + } + env.set_status(status); + } + pc += 1; + } + crate::parse::ast::Command::For(var, list) => { + let should = stack.should_execute(); + if !stack.current_pc(pc) { + stack.push(Frame::For( + false, + pc, + if stack.should_execute() { + list.clone() + .into_iter() + .map(|w| async { + w.eval(env) + .await + .map(IntoIterator::into_iter) + }) + .collect::<futures_util::stream::FuturesOrdered<_>>() + .try_collect::<Vec<_>>().await? + .into_iter() + .flatten() + .collect() + } else { + vec![] + }, + )); + } + if should { + if let Some(Frame::For(should, _, list)) = stack.top_mut() + { + *should = !list.is_empty(); + if *should { + let val = list.remove(0); + // XXX i really need to just pick one location and + // stick with it instead of trying to keep these + // in sync + env.set_var(var, &val); + std::env::set_var(var, &val); + } + } else { + unreachable!(); + } + } + pc += 1; + } + crate::parse::ast::Command::Else(pipeline) => { + let mut top = stack.pop(); + if stack.should_execute() { + if let Frame::If(ref mut should, ref mut found) = top { + if *found { + *should = false; + } else if let Some(pipeline) = pipeline { + let status = env.latest_status(); + run_pipeline( + pipeline.clone(), + env, + config, + shell_write, + ) + .await?; + *should = env.latest_status().success(); + if *should { + *found = true; + } + env.set_status(status); + } else { + *should = true; + *found = true; + } + } else { + todo!(); + } + } + stack.push(top); + pc += 1; + } + crate::parse::ast::Command::End => match stack.top() { + Some(Frame::If(..)) => { + stack.pop(); + pc += 1; + } + Some( + Frame::While(should, start) + | Frame::For(should, start, _), + ) => { + if *should { + pc = *start; + } else { + stack.pop(); + pc += 1; + } + } + None => todo!(), + }, + } + } + Ok(()) +} + +async fn run_pipeline( + pipeline: crate::parse::ast::Pipeline, + env: &mut Env, + config: &crate::config::Config, + shell_write: &mut Option<tokio::fs::File>, +) -> Result<()> { + write_event(shell_write, Event::RunPipeline(pipeline.span())).await?; + // Safety: pipelines are run serially, so only one copy of these will ever + // exist at once. note that reusing a single copy of these at the top + // level would not be safe, because in the case of a command line like + // "echo foo; ls", we would pass the stdout fd to the ls process while it + // is still open here, and may still have data buffered. + let stdin = unsafe { std::fs::File::from_raw_fd(0) }; + let stdout = unsafe { std::fs::File::from_raw_fd(1) }; + let stderr = unsafe { std::fs::File::from_raw_fd(2) }; + let mut io = builtins::Io::new(); + io.set_stdin(stdin); + io.set_stdout(stdout); + io.set_stderr(stderr); + + let pwd = env.pwd().to_path_buf(); + let interactive = shell_write.is_some(); + let pipeline = pipeline.eval(env).await?; + let mut exes: Vec<_> = pipeline.into_exes().collect(); + for exe in &mut exes { + let mut seen = std::collections::HashSet::new(); + while let Some(alias) = config.alias_for(exe.exe()) { + let mut new = alias.clone().eval(env).await?; + let override_self = exe.exe() == new.exe(); + if seen.contains(new.exe()) { + return Err(anyhow!( + "recursive alias found: {}", + new.exe().display() + )); + } + seen.insert(new.exe().to_path_buf()); + new.append(exe.clone()); + *exe = new; + if override_self { + break; + } + } + } + let cmds = exes + .into_iter() + .map(|exe| Command::new(exe, io.clone())) + .collect(); + let (children, pg) = spawn_children(cmds, env, interactive)?; + let status = wait_children(children, pg, shell_write).await; + if interactive { + sys::set_foreground_pg(nix::unistd::getpid())?; + } + env.update()?; + env.set_status(status); + if env.pwd() != pwd { + env.set_prev_pwd(pwd); + } + Ok(()) +} + +async fn write_event( + fh: &mut Option<tokio::fs::File>, + event: Event, +) -> Result<()> { + if let Some(fh) = fh { + fh.write_all(&bincode::serialize(&event)?).await?; + fh.flush().await?; + } + Ok(()) +} + +fn spawn_children( + mut cmds: Vec<Command>, + env: &Env, + interactive: bool, +) -> Result<(Vec<Child>, Option<nix::unistd::Pid>)> { + for i in 0..(cmds.len() - 1) { + let (r, w) = sys::pipe()?; + cmds[i].stdout(w); + cmds[i + 1].stdin(r); + } + + let mut children = vec![]; + let mut pg_pid = None; + for mut cmd in cmds { + // Safety: setpgid is an async-signal-safe function + unsafe { + cmd.pre_exec(move || { + sys::setpgid_child(pg_pid)?; + Ok(()) + }); + } + let child = cmd.spawn(env)?; + if let Some(id) = child.id() { + let child_pid = sys::id_to_pid(id); + sys::setpgid_parent(child_pid, pg_pid)?; + if pg_pid.is_none() { + pg_pid = Some(child_pid); + if interactive { + sys::set_foreground_pg(child_pid)?; + } + } + } + children.push(child); + } + Ok((children, pg_pid)) +} + +async fn wait_children( + children: Vec<Child>, + pg: Option<nix::unistd::Pid>, + shell_write: &mut Option<tokio::fs::File>, +) -> std::process::ExitStatus { + enum Res { + Child(nix::Result<nix::sys::wait::WaitStatus>), + Builtin((Result<std::process::ExitStatus>, bool)), + } + + macro_rules! bail { + ($e:expr) => { + eprintln!("nbsh: {}\n", $e); + return std::process::ExitStatus::from_raw(1 << 8); + }; + } + + let mut final_status = None; + + let count = children.len(); + let (children, builtins): (Vec<_>, Vec<_>) = children + .into_iter() + .enumerate() + .partition(|(_, child)| child.id().is_some()); + let mut children: std::collections::HashMap<_, _> = children + .into_iter() + .map(|(i, child)| { + (sys::id_to_pid(child.id().unwrap()), (child, i == count - 1)) + }) + .collect(); + let mut builtin_count = builtins.len(); + let builtins: futures_util::stream::FuturesUnordered<_> = + builtins + .into_iter() + .map(|(i, child)| async move { + (child.status().await, i == count - 1) + }) + .collect(); + + let (wait_w, wait_r) = tokio::sync::mpsc::unbounded_channel(); + if let Some(pg) = pg { + tokio::task::spawn_blocking(move || loop { + let res = nix::sys::wait::waitpid( + sys::neg_pid(pg), + Some(nix::sys::wait::WaitPidFlag::WUNTRACED), + ); + match wait_w.send(res) { + Ok(_) => {} + Err(tokio::sync::mpsc::error::SendError(res)) => { + // we should never drop wait_r while there are still valid + // things to read + assert!(res.is_err()); + break; + } + } + }); + } + + let mut stream: futures_util::stream::SelectAll<_> = [ + tokio_stream::wrappers::UnboundedReceiverStream::new(wait_r) + .map(Res::Child) + .boxed(), + builtins.map(Res::Builtin).boxed(), + ] + .into_iter() + .collect(); + while let Some(res) = stream.next().await { + match res { + Res::Child(Ok(status)) => { + match status { + // we can't call child.status() here to unify these + // branches because our waitpid call already collected the + // status + nix::sys::wait::WaitStatus::Exited(pid, code) => { + let (_, last) = children.remove(&pid).unwrap(); + if last { + final_status = Some( + std::process::ExitStatus::from_raw(code << 8), + ); + } + } + nix::sys::wait::WaitStatus::Signaled(pid, signal, _) => { + let (_, last) = children.remove(&pid).unwrap(); + if signal == nix::sys::signal::Signal::SIGINT { + if let Err(e) = nix::sys::signal::raise( + nix::sys::signal::Signal::SIGINT, + ) { + bail!(e); + } + } + // this conversion is safe because the Signal enum is + // repr(i32) + #[allow(clippy::as_conversions)] + if last { + final_status = + Some(std::process::ExitStatus::from_raw( + signal as i32, + )); + } + } + nix::sys::wait::WaitStatus::Stopped(pid, signal) => { + if signal == nix::sys::signal::Signal::SIGTSTP { + if let Err(e) = + write_event(shell_write, Event::Suspend).await + { + bail!(e); + } + if let Err(e) = nix::sys::signal::kill( + pid, + nix::sys::signal::Signal::SIGCONT, + ) { + bail!(e); + } + } + } + _ => {} + } + } + Res::Child(Err(e)) => { + bail!(e); + } + Res::Builtin((Ok(status), last)) => { + // this conversion is safe because the Signal enum is + // repr(i32) + #[allow(clippy::as_conversions)] + if status.signal() + == Some(nix::sys::signal::Signal::SIGINT as i32) + { + if let Err(e) = nix::sys::signal::raise( + nix::sys::signal::Signal::SIGINT, + ) { + bail!(e); + } + } + if last { + final_status = Some(status); + } + builtin_count -= 1; + } + Res::Builtin((Err(e), _)) => { + bail!(e); + } + } + + if children.is_empty() && builtin_count == 0 { + break; + } + } + + final_status.unwrap() +} diff --git a/src/runner/prelude.rs b/src/runner/prelude.rs new file mode 100644 index 0000000..53b67fc --- /dev/null +++ b/src/runner/prelude.rs @@ -0,0 +1 @@ +pub use crate::prelude::*; diff --git a/src/runner/sys.rs b/src/runner/sys.rs new file mode 100644 index 0000000..b6a9428 --- /dev/null +++ b/src/runner/sys.rs @@ -0,0 +1,79 @@ +use crate::runner::prelude::*; + +const PID0: nix::unistd::Pid = nix::unistd::Pid::from_raw(0); + +pub fn pipe() -> Result<(std::fs::File, std::fs::File)> { + let (r, w) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?; + // Safety: these file descriptors were just returned by pipe2 above, and + // are only available in this function, so nothing else can be accessing + // them + Ok((unsafe { std::fs::File::from_raw_fd(r) }, unsafe { + std::fs::File::from_raw_fd(w) + })) +} + +pub fn set_foreground_pg(pg: nix::unistd::Pid) -> Result<()> { + let pty = nix::fcntl::open( + "/dev/tty", + nix::fcntl::OFlag::empty(), + nix::sys::stat::Mode::empty(), + )?; + + // if a background process calls tcsetpgrp, the kernel will send it + // SIGTTOU which suspends it. if that background process is the session + // leader and doesn't have SIGTTOU blocked, the kernel will instead just + // return ENOTTY from the tcsetpgrp call rather than sending a signal to + // avoid deadlocking the process. therefore, we need to ensure that + // SIGTTOU is blocked here. + + // Safety: setting a signal handler to SigIgn is always safe + unsafe { + nix::sys::signal::signal( + nix::sys::signal::Signal::SIGTTOU, + nix::sys::signal::SigHandler::SigIgn, + )?; + } + let res = nix::unistd::tcsetpgrp(pty, pg); + // Safety: setting a signal handler to SigDfl is always safe + unsafe { + nix::sys::signal::signal( + nix::sys::signal::Signal::SIGTTOU, + nix::sys::signal::SigHandler::SigDfl, + )?; + } + res?; + + nix::unistd::close(pty)?; + + nix::sys::signal::kill(neg_pid(pg), nix::sys::signal::Signal::SIGCONT) + // the process group has already exited + .allow(nix::errno::Errno::ESRCH)?; + + Ok(()) +} + +pub fn setpgid_child(pg: Option<nix::unistd::Pid>) -> std::io::Result<()> { + nix::unistd::setpgid(PID0, pg.unwrap_or(PID0))?; + Ok(()) +} + +pub fn setpgid_parent( + pid: nix::unistd::Pid, + pg: Option<nix::unistd::Pid>, +) -> Result<()> { + nix::unistd::setpgid(pid, pg.unwrap_or(PID0)) + // the child already called exec, so it must have already called + // setpgid itself + .allow(nix::errno::Errno::EACCES) + // the child already exited, so we don't care + .allow(nix::errno::Errno::ESRCH)?; + Ok(()) +} + +pub fn id_to_pid(id: u32) -> nix::unistd::Pid { + nix::unistd::Pid::from_raw(id.try_into().unwrap()) +} + +pub fn neg_pid(pid: nix::unistd::Pid) -> nix::unistd::Pid { + nix::unistd::Pid::from_raw(-pid.as_raw()) +} diff --git a/src/shell.pest b/src/shell.pest new file mode 100644 index 0000000..92b173a --- /dev/null +++ b/src/shell.pest @@ -0,0 +1,72 @@ +basic_escape_char = @{ "\\\\" | "\\'" } +escape_char = @{ "\\" ~ ANY } + +bareword_char = @{ + escape_char | + !("|" | ";" | "\"" | "'" | "$" | "{" | "(" | ")" | WHITESPACE | COMMENT) + ~ ANY +} +single_string_char = @{ basic_escape_char | (!"'" ~ ANY) } +double_string_char = @{ escape_char | (!("\"" | "$") ~ ANY) } + +var = @{ + ("$" ~ XID_START ~ XID_CONTINUE*) | + ("$" ~ ("?" | "$" | "*" | ASCII_DIGIT)) | + ("${" ~ (!"}" ~ ANY)+ ~ "}") +} +bareword = @{ bareword_char+ } +single_string = @{ single_string_char+ } +double_string = @{ double_string_char+ } + +alternation_bareword_char = @{ !("," | "}") ~ bareword_char } +alternation_bareword = @{ alternation_bareword_char+ } +alternation_word_part = ${ + var | + alternation_bareword | + "'" ~ single_string? ~ "'" | + "\"" ~ (var | double_string)* ~ "\"" +} +alternation_word = ${ alternation_word_part* } +alternation = ${ "{" ~ alternation_word ~ ("," ~ alternation_word)* ~ "}" } + +substitution = ${ "$(" ~ w? ~ commands ~ w? ~ ")"} + +word_part = ${ + alternation | + substitution | + var | + bareword | + "'" ~ single_string? ~ "'" | + "\"" ~ (substitution | var | double_string)* ~ "\"" +} +word = ${ word_part+ } + +redir_prefix = @{ + ("in" | "out" | "err" | ASCII_DIGIT*) ~ (">>" | ">" | "<") +} +redirect = ${ redir_prefix ~ w? ~ word } + +exe = ${ (redirect | word) ~ (w ~ (redirect | word))* } +subshell = ${ + "(" ~ w? ~ commands ~ w? ~ ")" ~ (w? ~ redirect ~ (w ~ redirect)*)? +} +list = ${ word ~ (w ~ word)* } +pipeline = ${ (subshell | exe) ~ (w? ~ "|" ~ w? ~ (subshell | exe))* } + +control_if = ${ "if" ~ w ~ pipeline } +control_while = ${ "while" ~ w ~ pipeline } +control_for = ${ "for" ~ w ~ bareword ~ w ~ "in" ~ w ~ list } +control_else = ${ "else" ~ (w ~ "if" ~ w ~ pipeline)? } +control_end = ${ "end" } +control = ${ + control_if | control_while | control_for | control_else | control_end +} + +command = ${ control | pipeline } +commands = ${ command ~ (w? ~ ";" ~ w? ~ command)* } + +line = ${ SOI ~ w? ~ commands ~ w? ~ EOI } + +w = _{ (WHITESPACE | COMMENT)+ } +WHITESPACE = _{ (" " | "\t" | "\n") } +COMMENT = _{ "#" ~ ANY* } diff --git a/src/shell/event.rs b/src/shell/event.rs new file mode 100644 index 0000000..dc58e6f --- /dev/null +++ b/src/shell/event.rs @@ -0,0 +1,163 @@ +use crate::prelude::*; + +#[derive(Debug)] +pub enum Event { + Key(textmode::Key), + Resize((u16, u16)), + PtyOutput, + ChildRunPipeline(usize, (usize, usize)), + ChildSuspend(usize), + ChildExit(usize, super::history::ExitInfo, Option<Env>), + GitInfo(Option<super::inputs::GitInfo>), + ClockTimer, +} + +pub fn channel() -> (Writer, Reader) { + let (event_w, event_r) = tokio::sync::mpsc::unbounded_channel(); + (Writer::new(event_w), Reader::new(event_r)) +} + +#[derive(Clone)] +pub struct Writer(tokio::sync::mpsc::UnboundedSender<Event>); + +impl Writer { + pub fn new(event_w: tokio::sync::mpsc::UnboundedSender<Event>) -> Self { + Self(event_w) + } + + pub fn send(&self, event: Event) { + // the only time this should ever error is when the application is + // shutting down, at which point we don't actually care about any + // further dropped messages + #[allow(clippy::let_underscore_drop)] + let _ = self.0.send(event); + } +} + +pub struct Reader(std::sync::Arc<InnerReader>); + +impl Reader { + pub fn new( + mut input: tokio::sync::mpsc::UnboundedReceiver<Event>, + ) -> Self { + let inner = std::sync::Arc::new(InnerReader::new()); + { + let inner = inner.clone(); + tokio::spawn(async move { + while let Some(event) = input.recv().await { + inner.new_event(Some(event)); + } + inner.new_event(None); + }); + } + Self(inner) + } + + pub async fn recv(&self) -> Option<Event> { + self.0.recv().await + } +} + +struct InnerReader { + pending: std::sync::Mutex<Pending>, + cvar: tokio::sync::Notify, +} + +impl InnerReader { + fn new() -> Self { + Self { + pending: std::sync::Mutex::new(Pending::new()), + cvar: tokio::sync::Notify::new(), + } + } + + async fn recv(&self) -> Option<Event> { + loop { + if let Some(event) = self.pending.lock().unwrap().get_event() { + return event; + } + self.cvar.notified().await; + } + } + + fn new_event(&self, event: Option<Event>) { + self.pending.lock().unwrap().new_event(event); + self.cvar.notify_one(); + } +} + +#[allow(clippy::option_option)] +#[derive(Default)] +struct Pending { + key: std::collections::VecDeque<textmode::Key>, + size: Option<(u16, u16)>, + pty_output: bool, + child_run_pipeline: std::collections::VecDeque<(usize, (usize, usize))>, + child_suspend: std::collections::VecDeque<usize>, + child_exit: Option<(usize, super::history::ExitInfo, Option<Env>)>, + git_info: Option<Option<super::inputs::GitInfo>>, + clock_timer: bool, + done: bool, +} + +impl Pending { + fn new() -> Self { + Self::default() + } + + fn get_event(&mut self) -> Option<Option<Event>> { + if self.done { + return Some(None); + } + if let Some(key) = self.key.pop_front() { + return Some(Some(Event::Key(key))); + } + if let Some(size) = self.size.take() { + return Some(Some(Event::Resize(size))); + } + if let Some((idx, span)) = self.child_run_pipeline.pop_front() { + return Some(Some(Event::ChildRunPipeline(idx, span))); + } + if let Some(idx) = self.child_suspend.pop_front() { + return Some(Some(Event::ChildSuspend(idx))); + } + if let Some((idx, exit_info, env)) = self.child_exit.take() { + return Some(Some(Event::ChildExit(idx, exit_info, env))); + } + if let Some(info) = self.git_info.take() { + return Some(Some(Event::GitInfo(info))); + } + if self.clock_timer { + self.clock_timer = false; + return Some(Some(Event::ClockTimer)); + } + // process_output should be last because it will often be the case + // that there is ~always new process output (cat on large files, yes, + // etc) and that shouldn't prevent other events from happening + if self.pty_output { + self.pty_output = false; + return Some(Some(Event::PtyOutput)); + } + None + } + + fn new_event(&mut self, event: Option<Event>) { + match event { + Some(Event::Key(key)) => self.key.push_back(key), + Some(Event::Resize(size)) => self.size = Some(size), + Some(Event::PtyOutput) => self.pty_output = true, + Some(Event::ChildRunPipeline(idx, span)) => { + self.child_run_pipeline.push_back((idx, span)); + } + Some(Event::ChildSuspend(idx)) => { + self.child_suspend.push_back(idx); + } + Some(Event::ChildExit(idx, exit_info, env)) => { + self.child_exit = Some((idx, exit_info, env)); + } + Some(Event::GitInfo(info)) => self.git_info = Some(info), + Some(Event::ClockTimer) => self.clock_timer = true, + None => self.done = true, + } + } +} diff --git a/src/shell/history/entry.rs b/src/shell/history/entry.rs new file mode 100644 index 0000000..0491bf7 --- /dev/null +++ b/src/shell/history/entry.rs @@ -0,0 +1,429 @@ +use crate::shell::prelude::*; + +pub struct Entry { + cmdline: String, + env: Env, + pty: super::pty::Pty, + fullscreen: Option<bool>, + start_instant: std::time::Instant, + start_time: time::OffsetDateTime, + state: State, +} + +impl Entry { + pub fn new( + cmdline: String, + env: Env, + size: (u16, u16), + event_w: crate::shell::event::Writer, + ) -> Result<Self> { + let start_instant = std::time::Instant::now(); + let start_time = time::OffsetDateTime::now_utc(); + + let (pty, pts) = super::pty::Pty::new(size, event_w.clone()).unwrap(); + let (child, fh) = Self::spawn_command(&cmdline, &env, &pts)?; + tokio::spawn(Self::task(child, fh, env.idx(), event_w)); + Ok(Self { + cmdline, + env, + pty, + fullscreen: None, + start_instant, + start_time, + state: State::Running((0, 0)), + }) + } + + pub fn render( + &self, + out: &mut impl textmode::Textmode, + entry_count: usize, + vt: &mut super::pty::Vt, + focused: bool, + scrolling: bool, + offset: time::UtcOffset, + ) { + let idx = self.env.idx(); + let size = out.screen().size(); + let time = self.state.exit_info().map_or_else( + || { + format!( + "[{}]", + crate::format::time(self.start_time.to_offset(offset)) + ) + }, + |info| { + format!( + "({}) [{}]", + crate::format::duration( + info.instant - self.start_instant + ), + crate::format::time(self.start_time.to_offset(offset)), + ) + }, + ); + + if vt.bell(focused) { + out.write(b"\x07"); + } + + Self::set_bgcolor(out, idx, focused); + out.set_fgcolor(textmode::color::YELLOW); + let entry_count_width = format!("{}", entry_count + 1).len(); + let idx_str = format!("{}", idx + 1); + out.write_str(&" ".repeat(entry_count_width - idx_str.len())); + out.write_str(&idx_str); + out.write_str(" "); + out.reset_attributes(); + + Self::set_bgcolor(out, idx, focused); + if let Some(info) = self.state.exit_info() { + if info.status.signal().is_some() { + out.set_fgcolor(textmode::color::MAGENTA); + } else if info.status.success() { + out.set_fgcolor(textmode::color::DARKGREY); + } else { + out.set_fgcolor(textmode::color::RED); + } + out.write_str(&crate::format::exit_status(info.status)); + } else { + out.write_str(" "); + } + out.reset_attributes(); + + if vt.is_bell() { + out.set_bgcolor(textmode::Color::Rgb(64, 16, 16)); + } else { + Self::set_bgcolor(out, idx, focused); + } + out.write_str("$ "); + Self::set_bgcolor(out, idx, focused); + let start = usize::from(out.screen().cursor_position().1); + let end = usize::from(size.1) - time.len() - 2; + let max_len = end - start; + let cmd = if self.cmd().len() > max_len { + &self.cmd()[..(max_len - 4)] + } else { + self.cmd() + }; + if let State::Running(span) = self.state { + let span = (span.0.min(cmd.len()), span.1.min(cmd.len())); + if !cmd[..span.0].is_empty() { + out.write_str(&cmd[..span.0]); + } + if !cmd[span.0..span.1].is_empty() { + out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); + out.write_str(&cmd[span.0..span.1]); + Self::set_bgcolor(out, idx, focused); + } + if !cmd[span.1..].is_empty() { + out.write_str(&cmd[span.1..]); + } + } else { + out.write_str(cmd); + } + if self.cmd().len() > max_len { + if let State::Running(span) = self.state { + if span.0 < cmd.len() && span.1 > cmd.len() { + out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); + } + } + out.write_str(" "); + if let State::Running(span) = self.state { + if span.1 > cmd.len() { + out.set_bgcolor(textmode::Color::Rgb(16, 64, 16)); + } + } + out.set_fgcolor(textmode::color::BLUE); + out.write_str("..."); + } + out.reset_attributes(); + + Self::set_bgcolor(out, idx, focused); + let cur_pos = out.screen().cursor_position(); + out.write_str(&" ".repeat( + usize::from(size.1) - time.len() - 1 - usize::from(cur_pos.1), + )); + out.write_str(&time); + out.write_str(" "); + out.reset_attributes(); + + if vt.binary() { + let msg = "This appears to be binary data. Fullscreen this entry to view anyway."; + let len: u16 = msg.len().try_into().unwrap(); + out.move_to( + out.screen().cursor_position().0 + 1, + (size.1 - len) / 2, + ); + out.set_fgcolor(textmode::color::RED); + out.write_str(msg); + out.hide_cursor(true); + } else { + let last_row = + vt.output_lines(focused && !scrolling, self.state.running()); + let mut max_lines = self.max_lines(entry_count); + if last_row > max_lines { + out.write(b"\r\n"); + out.set_fgcolor(textmode::color::BLUE); + out.write_str("..."); + out.reset_attributes(); + max_lines -= 1; + } + let mut out_row = out.screen().cursor_position().0 + 1; + let screen = vt.screen(); + let pos = screen.cursor_position(); + let mut wrapped = false; + let mut cursor_found = None; + for (idx, row) in screen + .rows_formatted(0, size.1) + .enumerate() + .take(last_row) + .skip(last_row.saturating_sub(max_lines)) + { + let idx: u16 = idx.try_into().unwrap(); + out.reset_attributes(); + if !wrapped { + out.move_to(out_row, 0); + } + out.write(&row); + wrapped = screen.row_wrapped(idx); + if pos.0 == idx { + cursor_found = Some(out_row); + } + out_row += 1; + } + if focused && !scrolling { + if let Some(row) = cursor_found { + out.hide_cursor(screen.hide_cursor()); + out.move_to(row, pos.1); + } else { + out.hide_cursor(true); + } + } + } + + out.reset_attributes(); + } + + pub fn render_fullscreen(&self, out: &mut impl textmode::Textmode) { + self.pty.with_vt_mut(|vt| { + out.write(&vt.screen().state_formatted()); + if vt.bell(true) { + out.write(b"\x07"); + } + out.reset_attributes(); + }); + } + + pub fn input(&self, bytes: Vec<u8>) { + self.pty.input(bytes); + } + + pub fn resize(&self, size: (u16, u16)) { + self.pty.resize(size); + } + + pub fn cmd(&self) -> &str { + &self.cmdline + } + + pub fn start_time(&self) -> time::OffsetDateTime { + self.start_time + } + + pub fn toggle_fullscreen(&mut self) { + if let Some(fullscreen) = self.fullscreen { + self.fullscreen = Some(!fullscreen); + } else { + self.fullscreen = Some(!self.pty.fullscreen()); + } + } + + pub fn set_fullscreen(&mut self, fullscreen: bool) { + self.fullscreen = Some(fullscreen); + } + + pub fn running(&self) -> bool { + self.state.running() + } + + pub fn exited(&mut self, exit_info: ExitInfo) { + self.state = State::Exited(exit_info); + } + + pub fn lines(&self, entry_count: usize, focused: bool) -> usize { + let running = self.running(); + 1 + std::cmp::min( + self.pty.with_vt(|vt| vt.output_lines(focused, running)), + self.max_lines(entry_count), + ) + } + + pub fn should_fullscreen(&self) -> bool { + self.fullscreen.unwrap_or_else(|| self.pty.fullscreen()) + } + + pub fn lock_vt(&self) -> std::sync::MutexGuard<super::pty::Vt> { + self.pty.lock_vt() + } + + pub fn set_span(&mut self, new_span: (usize, usize)) { + if let State::Running(ref mut span) = self.state { + *span = new_span; + } + } + + fn max_lines(&self, entry_count: usize) -> usize { + if self.env.idx() == entry_count - 1 { + 15 + } else { + 5 + } + } + + fn set_bgcolor( + out: &mut impl textmode::Textmode, + idx: usize, + focus: bool, + ) { + if focus { + out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b)); + } else if idx % 2 == 0 { + out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00)); + } else { + out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20)); + } + } + + fn spawn_command( + cmdline: &str, + env: &Env, + pts: &pty_process::Pts, + ) -> Result<(tokio::process::Child, std::fs::File)> { + let mut cmd = pty_process::Command::new(crate::info::current_exe()?); + cmd.args(&["-c", cmdline, "--status-fd", "3"]); + env.apply(&mut cmd); + let (from_r, from_w) = + nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?; + // Safety: from_r was just opened above and is not used anywhere else + let fh = unsafe { std::fs::File::from_raw_fd(from_r) }; + // Safety: dup2 is an async-signal-safe function + unsafe { + cmd.pre_exec(move || { + nix::unistd::dup2(from_w, 3)?; + Ok(()) + }); + } + let child = cmd.spawn(pts)?; + nix::unistd::close(from_w)?; + Ok((child, fh)) + } + + async fn task( + mut child: tokio::process::Child, + fh: std::fs::File, + idx: usize, + event_w: crate::shell::event::Writer, + ) { + enum Res { + Read(crate::runner::Event), + Exit(std::io::Result<std::process::ExitStatus>), + } + + let (read_w, read_r) = tokio::sync::mpsc::unbounded_channel(); + tokio::task::spawn_blocking(move || loop { + let event = bincode::deserialize_from(&fh); + match event { + Ok(event) => { + read_w.send(event).unwrap(); + } + Err(e) => { + match &*e { + bincode::ErrorKind::Io(io_e) => { + assert!( + io_e.kind() + == std::io::ErrorKind::UnexpectedEof + ); + } + e => { + panic!("{}", e); + } + } + break; + } + } + }); + + let mut stream: futures_util::stream::SelectAll<_> = [ + tokio_stream::wrappers::UnboundedReceiverStream::new(read_r) + .map(Res::Read) + .boxed(), + futures_util::stream::once(child.wait()) + .map(Res::Exit) + .boxed(), + ] + .into_iter() + .collect(); + let mut exit_status = None; + let mut new_env = None; + while let Some(res) = stream.next().await { + match res { + Res::Read(event) => match event { + crate::runner::Event::RunPipeline(new_span) => { + // we could just update the span in place here, but we + // do this as an event so that we can also trigger a + // refresh + event_w.send(Event::ChildRunPipeline(idx, new_span)); + } + crate::runner::Event::Suspend => { + event_w.send(Event::ChildSuspend(idx)); + } + crate::runner::Event::Exit(env) => { + new_env = Some(env); + } + }, + Res::Exit(status) => { + exit_status = Some(status.unwrap()); + } + } + } + event_w.send(Event::ChildExit( + idx, + ExitInfo::new(exit_status.unwrap()), + new_env, + )); + } +} + +enum State { + Running((usize, usize)), + Exited(ExitInfo), +} + +impl State { + fn exit_info(&self) -> Option<&ExitInfo> { + match self { + Self::Running(_) => None, + Self::Exited(exit_info) => Some(exit_info), + } + } + + fn running(&self) -> bool { + self.exit_info().is_none() + } +} + +#[derive(Debug)] +pub struct ExitInfo { + status: std::process::ExitStatus, + instant: std::time::Instant, +} + +impl ExitInfo { + fn new(status: std::process::ExitStatus) -> Self { + Self { + status, + instant: std::time::Instant::now(), + } + } +} diff --git a/src/shell/history/mod.rs b/src/shell/history/mod.rs new file mode 100644 index 0000000..91149c1 --- /dev/null +++ b/src/shell/history/mod.rs @@ -0,0 +1,208 @@ +use crate::shell::prelude::*; + +mod entry; +pub use entry::{Entry, ExitInfo}; +mod pty; + +pub struct History { + size: (u16, u16), + entries: Vec<Entry>, + scroll_pos: usize, +} + +impl History { + pub fn new() -> Self { + Self { + size: (24, 80), + entries: vec![], + scroll_pos: 0, + } + } + + pub fn render( + &self, + out: &mut impl textmode::Textmode, + repl_lines: usize, + focus: Option<usize>, + scrolling: bool, + offset: time::UtcOffset, + ) { + let mut cursor = None; + for (idx, used_lines, mut vt) in + self.visible(repl_lines, focus, scrolling).rev() + { + let focused = focus.map_or(false, |focus| idx == focus); + out.move_to( + (usize::from(self.size.0) - used_lines).try_into().unwrap(), + 0, + ); + self.entries[idx].render( + out, + self.entry_count(), + &mut *vt, + focused, + scrolling, + offset, + ); + if focused && !scrolling { + cursor = Some(( + out.screen().cursor_position(), + out.screen().hide_cursor(), + )); + } + } + if let Some((pos, hide)) = cursor { + out.move_to(pos.0, pos.1); + out.hide_cursor(hide); + } + } + + pub fn entry(&self, idx: usize) -> &Entry { + &self.entries[idx] + } + + pub fn entry_mut(&mut self, idx: usize) -> &mut Entry { + &mut self.entries[idx] + } + + pub fn resize(&mut self, size: (u16, u16)) { + self.size = size; + for entry in &self.entries { + entry.resize(size); + } + } + + pub fn run( + &mut self, + cmdline: String, + env: Env, + event_w: crate::shell::event::Writer, + ) { + self.entries + .push(Entry::new(cmdline, env, self.size, event_w).unwrap()); + } + + pub fn entry_count(&self) -> usize { + self.entries.len() + } + + pub fn make_focus_visible( + &mut self, + repl_lines: usize, + focus: Option<usize>, + scrolling: bool, + ) { + if self.entries.is_empty() || focus.is_none() { + return; + } + let focus = focus.unwrap(); + + let mut done = false; + while focus + < self + .visible(repl_lines, Some(focus), scrolling) + .map(|(idx, ..)| idx) + .next() + .unwrap() + { + self.scroll_pos += 1; + done = true; + } + if done { + return; + } + + while focus + > self + .visible(repl_lines, Some(focus), scrolling) + .map(|(idx, ..)| idx) + .last() + .unwrap() + { + self.scroll_pos -= 1; + } + } + + pub async fn save(&self) { + // TODO: we'll probably want some amount of flock or something here + let mut fh = tokio::fs::OpenOptions::new() + .append(true) + .open(crate::dirs::history_file()) + .await + .unwrap(); + for entry in &self.entries { + fh.write_all( + format!( + ": {}:0;{}\n", + entry.start_time().unix_timestamp(), + entry.cmd() + ) + .as_bytes(), + ) + .await + .unwrap(); + } + } + + fn visible( + &self, + repl_lines: usize, + focus: Option<usize>, + scrolling: bool, + ) -> VisibleEntries { + let mut iter = VisibleEntries::new(); + let mut used_lines = repl_lines; + for (idx, entry) in + self.entries.iter().enumerate().rev().skip(self.scroll_pos) + { + let focused = focus.map_or(false, |focus| idx == focus); + used_lines += + entry.lines(self.entry_count(), focused && !scrolling); + if used_lines > usize::from(self.size.0) { + break; + } + iter.add(idx, used_lines, entry.lock_vt()); + } + iter + } +} + +struct VisibleEntries<'a> { + entries: std::collections::VecDeque<( + usize, + usize, + std::sync::MutexGuard<'a, pty::Vt>, + )>, +} + +impl<'a> VisibleEntries<'a> { + fn new() -> Self { + Self { + entries: std::collections::VecDeque::new(), + } + } + + fn add( + &mut self, + idx: usize, + offset: usize, + vt: std::sync::MutexGuard<'a, pty::Vt>, + ) { + // push_front because we are adding them in reverse order + self.entries.push_front((idx, offset, vt)); + } +} + +impl<'a> std::iter::Iterator for VisibleEntries<'a> { + type Item = (usize, usize, std::sync::MutexGuard<'a, pty::Vt>); + + fn next(&mut self) -> Option<Self::Item> { + self.entries.pop_front() + } +} + +impl<'a> std::iter::DoubleEndedIterator for VisibleEntries<'a> { + fn next_back(&mut self) -> Option<Self::Item> { + self.entries.pop_back() + } +} diff --git a/src/shell/history/pty.rs b/src/shell/history/pty.rs new file mode 100644 index 0000000..cef4ca9 --- /dev/null +++ b/src/shell/history/pty.rs @@ -0,0 +1,196 @@ +use crate::shell::prelude::*; + +#[derive(Debug)] +enum Request { + Input(Vec<u8>), + Resize(u16, u16), +} + +pub struct Pty { + vt: std::sync::Arc<std::sync::Mutex<Vt>>, + request_w: tokio::sync::mpsc::UnboundedSender<Request>, +} + +impl Pty { + pub fn new( + size: (u16, u16), + event_w: crate::shell::event::Writer, + ) -> Result<(Self, pty_process::Pts)> { + let (request_w, request_r) = tokio::sync::mpsc::unbounded_channel(); + + let pty = pty_process::Pty::new()?; + pty.resize(pty_process::Size::new(size.0, size.1))?; + let pts = pty.pts()?; + + let vt = std::sync::Arc::new(std::sync::Mutex::new(Vt::new(size))); + + tokio::spawn(Self::task( + pty, + std::sync::Arc::clone(&vt), + request_r, + event_w, + )); + + Ok((Self { vt, request_w }, pts)) + } + + pub fn with_vt<T>(&self, f: impl FnOnce(&Vt) -> T) -> T { + let vt = self.vt.lock().unwrap(); + f(&*vt) + } + + pub fn with_vt_mut<T>(&self, f: impl FnOnce(&mut Vt) -> T) -> T { + let mut vt = self.vt.lock().unwrap(); + f(&mut *vt) + } + + pub fn lock_vt(&self) -> std::sync::MutexGuard<Vt> { + self.vt.lock().unwrap() + } + + pub fn fullscreen(&self) -> bool { + self.with_vt(|vt| vt.screen().alternate_screen()) + } + + pub fn input(&self, bytes: Vec<u8>) { + #[allow(clippy::let_underscore_drop)] + let _ = self.request_w.send(Request::Input(bytes)); + } + + pub fn resize(&self, size: (u16, u16)) { + #[allow(clippy::let_underscore_drop)] + let _ = self.request_w.send(Request::Resize(size.0, size.1)); + } + + async fn task( + pty: pty_process::Pty, + vt: std::sync::Arc<std::sync::Mutex<Vt>>, + request_r: tokio::sync::mpsc::UnboundedReceiver<Request>, + event_w: crate::shell::event::Writer, + ) { + enum Res { + Read(Result<bytes::Bytes, std::io::Error>), + Request(Request), + } + + let (pty_r, mut pty_w) = pty.into_split(); + let mut stream: futures_util::stream::SelectAll<_> = [ + tokio_util::io::ReaderStream::new(pty_r) + .map(Res::Read) + .boxed(), + tokio_stream::wrappers::UnboundedReceiverStream::new(request_r) + .map(Res::Request) + .boxed(), + ] + .into_iter() + .collect(); + while let Some(res) = stream.next().await { + match res { + Res::Read(res) => match res { + Ok(bytes) => { + vt.lock().unwrap().process(&bytes); + event_w.send(Event::PtyOutput); + } + Err(e) => { + // this means that there are no longer any open pts + // fds. we could alternately signal this through an + // explicit channel at ChildExit time, but this seems + // reliable enough. + if e.raw_os_error() == Some(libc::EIO) { + return; + } + panic!("pty read failed: {:?}", e); + } + }, + Res::Request(Request::Input(bytes)) => { + pty_w.write(&bytes).await.unwrap(); + } + Res::Request(Request::Resize(row, col)) => { + pty_w.resize(pty_process::Size::new(row, col)).unwrap(); + vt.lock().unwrap().set_size((row, col)); + } + } + } + } +} + +pub struct Vt { + vt: vt100::Parser, + bell_state: usize, + bell: bool, + real_bell_pending: bool, +} + +impl Vt { + pub fn new(size: (u16, u16)) -> Self { + Self { + vt: vt100::Parser::new(size.0, size.1, 0), + bell_state: 0, + bell: false, + real_bell_pending: false, + } + } + + pub fn process(&mut self, bytes: &[u8]) { + self.vt.process(bytes); + let screen = self.vt.screen(); + + let new_bell_state = screen.audible_bell_count(); + if new_bell_state != self.bell_state { + self.bell = true; + self.real_bell_pending = true; + self.bell_state = new_bell_state; + } + } + + pub fn screen(&self) -> &vt100::Screen { + self.vt.screen() + } + + pub fn set_size(&mut self, size: (u16, u16)) { + self.vt.set_size(size.0, size.1); + } + + pub fn is_bell(&self) -> bool { + self.bell + } + + pub fn bell(&mut self, focused: bool) -> bool { + let mut should = false; + if self.real_bell_pending { + if self.bell { + should = true; + } + self.real_bell_pending = false; + } + if focused { + self.bell = false; + } + should + } + + pub fn binary(&self) -> bool { + self.vt.screen().errors() > 5 + } + + pub fn output_lines(&self, focused: bool, running: bool) -> usize { + if self.binary() { + return 1; + } + + let screen = self.vt.screen(); + let mut last_row = 0; + for (idx, row) in screen.rows(0, screen.size().1).enumerate() { + if !row.is_empty() { + last_row = idx + 1; + } + } + if focused && running { + last_row = std::cmp::max( + last_row, + usize::from(screen.cursor_position().0) + 1, + ); + } + last_row + } +} diff --git a/src/shell/inputs/clock.rs b/src/shell/inputs/clock.rs new file mode 100644 index 0000000..250466e --- /dev/null +++ b/src/shell/inputs/clock.rs @@ -0,0 +1,27 @@ +use crate::shell::prelude::*; + +pub struct Handler; + +impl Handler { + pub fn new(event_w: crate::shell::event::Writer) -> Self { + tokio::spawn(Self::task(event_w)); + Self + } + + async fn task(event_w: crate::shell::event::Writer) { + let now_clock = time::OffsetDateTime::now_utc(); + let now_instant = tokio::time::Instant::now(); + let mut interval = tokio::time::interval_at( + now_instant + + std::time::Duration::from_nanos( + 1_000_000_000_u64 + .saturating_sub(now_clock.nanosecond().into()), + ), + std::time::Duration::from_secs(1), + ); + loop { + interval.tick().await; + event_w.send(Event::ClockTimer); + } + } +} diff --git a/src/shell/inputs/git.rs b/src/shell/inputs/git.rs new file mode 100644 index 0000000..dbae1c4 --- /dev/null +++ b/src/shell/inputs/git.rs @@ -0,0 +1,274 @@ +use crate::shell::prelude::*; + +use notify::Watcher as _; + +pub struct Handler { + git_w: tokio::sync::mpsc::UnboundedSender<std::path::PathBuf>, +} + +impl Handler { + pub fn new(event_w: crate::shell::event::Writer) -> Self { + let (git_w, git_r) = tokio::sync::mpsc::unbounded_channel(); + tokio::spawn(Self::task(git_r, event_w)); + Self { git_w } + } + + pub fn new_dir(&self, path: std::path::PathBuf) { + self.git_w.send(path).unwrap(); + } + + async fn task( + mut git_r: tokio::sync::mpsc::UnboundedReceiver<std::path::PathBuf>, + event_w: crate::shell::event::Writer, + ) { + // clippy can't tell that we assign to this later + #[allow(clippy::no_effect_underscore_binding)] + let mut _active_watcher = None; + while let Some(mut dir) = git_r.recv().await { + while let Ok(newer_dir) = git_r.try_recv() { + dir = newer_dir; + } + let repo = git2::Repository::discover(&dir).ok(); + if repo.is_some() { + let (sync_watch_w, sync_watch_r) = std::sync::mpsc::channel(); + let (watch_w, mut watch_r) = + tokio::sync::mpsc::unbounded_channel(); + let mut watcher = + notify::recommended_watcher(sync_watch_w).unwrap(); + watcher + .watch(&dir, notify::RecursiveMode::Recursive) + .unwrap(); + tokio::task::spawn_blocking(move || { + while let Ok(event) = sync_watch_r.recv() { + if watch_w.send(event).is_err() { + break; + } + } + }); + let event_w = event_w.clone(); + tokio::spawn(async move { + while watch_r.recv().await.is_some() { + let repo = git2::Repository::discover(&dir).ok(); + let info = tokio::task::spawn_blocking(|| { + repo.map(|repo| Info::new(&repo)) + }) + .await + .unwrap(); + event_w.send(Event::GitInfo(info)); + } + }); + _active_watcher = Some(watcher); + } else { + _active_watcher = None; + } + let info = tokio::task::spawn_blocking(|| { + repo.map(|repo| Info::new(&repo)) + }) + .await + .unwrap(); + event_w.send(Event::GitInfo(info)); + } + } +} + +#[derive(Debug)] +pub struct Info { + modified_files: bool, + staged_files: bool, + new_files: bool, + commits: bool, + active_operation: ActiveOperation, + branch: Option<String>, + remote_branch_diff: Option<(usize, usize)>, +} + +const MODIFIED: git2::Status = git2::Status::WT_DELETED + .union(git2::Status::WT_MODIFIED) + .union(git2::Status::WT_RENAMED) + .union(git2::Status::WT_TYPECHANGE) + .union(git2::Status::CONFLICTED); +const STAGED: git2::Status = git2::Status::INDEX_DELETED + .union(git2::Status::INDEX_MODIFIED) + .union(git2::Status::INDEX_NEW) + .union(git2::Status::INDEX_RENAMED) + .union(git2::Status::INDEX_TYPECHANGE); +const NEW: git2::Status = git2::Status::WT_NEW; + +impl Info { + pub fn new(git: &git2::Repository) -> Self { + let mut status_options = git2::StatusOptions::new(); + status_options.include_untracked(true); + status_options.update_index(true); + + let statuses = git.statuses(Some(&mut status_options)); + + let mut modified_files = false; + let mut staged_files = false; + let mut new_files = false; + if let Ok(statuses) = statuses { + for file in statuses.iter() { + if file.status().intersects(MODIFIED) { + modified_files = true; + } + if file.status().intersects(STAGED) { + staged_files = true; + } + if file.status().intersects(NEW) { + new_files = true; + } + } + } + + let head = git.head(); + let mut commits = false; + let mut branch = None; + let mut remote_branch_diff = None; + + if let Ok(head) = head { + commits = true; + if head.is_branch() { + branch = head.shorthand().map(ToString::to_string); + remote_branch_diff = + head.resolve() + .ok() + .map(|head| { + ( + head.target(), + head.shorthand().map(ToString::to_string), + ) + }) + .and_then(|(head_id, name)| { + head_id.and_then(|head_id| { + name.and_then(|name| { + git.refname_to_id(&format!( + "refs/remotes/origin/{}", + name + )) + .ok() + .and_then(|remote_id| { + git.graph_ahead_behind( + head_id, remote_id, + ) + .ok() + }) + }) + }) + }); + } else { + branch = + head.resolve().ok().and_then(|head| head.target()).map( + |oid| { + let mut sha: String = oid + .as_bytes() + .iter() + .take(4) + .map(|b| format!("{:02x}", b)) + .collect(); + sha.truncate(7); + sha + }, + ); + } + } + + let active_operation = match git.state() { + git2::RepositoryState::Merge => ActiveOperation::Merge, + git2::RepositoryState::Revert + | git2::RepositoryState::RevertSequence => { + ActiveOperation::Revert + } + git2::RepositoryState::CherryPick + | git2::RepositoryState::CherryPickSequence => { + ActiveOperation::CherryPick + } + git2::RepositoryState::Bisect => ActiveOperation::Bisect, + git2::RepositoryState::Rebase + | git2::RepositoryState::RebaseInteractive + | git2::RepositoryState::RebaseMerge => ActiveOperation::Rebase, + _ => ActiveOperation::None, + }; + + Self { + modified_files, + staged_files, + new_files, + commits, + active_operation, + branch, + remote_branch_diff, + } + } +} + +impl std::fmt::Display for Info { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "g")?; + + if self.modified_files { + write!(f, "*")?; + } + if self.staged_files { + write!(f, "+")?; + } + if self.new_files { + write!(f, "?")?; + } + if !self.commits { + write!(f, "!")?; + return Ok(()); + } + + let branch = self.branch.as_ref().map_or("???", |branch| { + if branch == "master" { + "" + } else { + branch + } + }); + if !branch.is_empty() { + write!(f, ":")?; + } + write!(f, "{}", branch)?; + + if let Some((local, remote)) = self.remote_branch_diff { + if local > 0 || remote > 0 { + write!(f, ":")?; + } + if local > 0 { + write!(f, "+{}", local)?; + } + if remote > 0 { + write!(f, "-{}", remote)?; + } + } else { + write!(f, ":-")?; + } + + write!(f, "{}", self.active_operation)?; + + Ok(()) + } +} + +#[derive(Debug, Copy, Clone)] +pub enum ActiveOperation { + None, + Merge, + Revert, + CherryPick, + Bisect, + Rebase, +} + +impl std::fmt::Display for ActiveOperation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ActiveOperation::None => Ok(()), + ActiveOperation::Merge => write!(f, "(m)"), + ActiveOperation::Revert => write!(f, "(v)"), + ActiveOperation::CherryPick => write!(f, "(c)"), + ActiveOperation::Bisect => write!(f, "(b)"), + ActiveOperation::Rebase => write!(f, "(r)"), + } + } +} diff --git a/src/shell/inputs/mod.rs b/src/shell/inputs/mod.rs new file mode 100644 index 0000000..48590a2 --- /dev/null +++ b/src/shell/inputs/mod.rs @@ -0,0 +1,32 @@ +use crate::shell::prelude::*; + +mod clock; +mod git; +pub use git::Info as GitInfo; +mod signals; +mod stdin; + +pub struct Handler { + _clock: clock::Handler, + git: git::Handler, + _signals: signals::Handler, + _stdin: stdin::Handler, +} + +impl Handler { + pub fn new( + input: textmode::blocking::Input, + event_w: crate::shell::event::Writer, + ) -> Result<Self> { + Ok(Self { + _clock: clock::Handler::new(event_w.clone()), + git: git::Handler::new(event_w.clone()), + _signals: signals::Handler::new(event_w.clone())?, + _stdin: stdin::Handler::new(input, event_w), + }) + } + + pub fn new_dir(&self, path: std::path::PathBuf) { + self.git.new_dir(path); + } +} diff --git a/src/shell/inputs/signals.rs b/src/shell/inputs/signals.rs new file mode 100644 index 0000000..4b91273 --- /dev/null +++ b/src/shell/inputs/signals.rs @@ -0,0 +1,30 @@ +use crate::shell::prelude::*; + +pub struct Handler; + +impl Handler { + pub fn new(event_w: crate::shell::event::Writer) -> Result<Self> { + let signals = tokio::signal::unix::signal( + tokio::signal::unix::SignalKind::window_change(), + )?; + tokio::spawn(Self::task(signals, event_w)); + Ok(Self) + } + + async fn task( + mut signals: tokio::signal::unix::Signal, + event_w: crate::shell::event::Writer, + ) { + event_w.send(resize_event()); + while signals.recv().await.is_some() { + event_w.send(resize_event()); + } + } +} + +fn resize_event() -> Event { + Event::Resize(terminal_size::terminal_size().map_or( + (24, 80), + |(terminal_size::Width(w), terminal_size::Height(h))| (h, w), + )) +} diff --git a/src/shell/inputs/stdin.rs b/src/shell/inputs/stdin.rs new file mode 100644 index 0000000..b966307 --- /dev/null +++ b/src/shell/inputs/stdin.rs @@ -0,0 +1,17 @@ +use crate::shell::prelude::*; + +pub struct Handler; + +impl Handler { + pub fn new( + mut input: textmode::blocking::Input, + event_w: crate::shell::event::Writer, + ) -> Self { + std::thread::spawn(move || { + while let Some(key) = input.read_key().unwrap() { + event_w.send(Event::Key(key)); + } + }); + Self + } +} diff --git a/src/shell/mod.rs b/src/shell/mod.rs new file mode 100644 index 0000000..fa7147b --- /dev/null +++ b/src/shell/mod.rs @@ -0,0 +1,484 @@ +use crate::shell::prelude::*; + +use textmode::Textmode as _; + +mod event; +mod history; +mod inputs; +mod old_history; +mod prelude; +mod readline; + +pub async fn main() -> Result<i32> { + let mut input = textmode::blocking::Input::new()?; + let mut output = textmode::Output::new().await?; + + // avoid the guards getting stuck in a task that doesn't run to + // completion + let _input_guard = input.take_raw_guard(); + let _output_guard = output.take_screen_guard(); + + let (event_w, event_r) = event::channel(); + + let inputs = inputs::Handler::new(input, event_w.clone()).unwrap(); + + let mut shell = Shell::new(crate::info::get_offset())?; + let mut prev_dir = shell.env.pwd().to_path_buf(); + inputs.new_dir(prev_dir.clone()); + while let Some(event) = event_r.recv().await { + match shell.handle_event(event, &event_w) { + Some(Action::Refresh) => { + shell.render(&mut output)?; + output.refresh().await?; + } + Some(Action::HardRefresh) => { + shell.render(&mut output)?; + output.hard_refresh().await?; + } + Some(Action::Resize(rows, cols)) => { + output.set_size(rows, cols); + shell.render(&mut output)?; + output.hard_refresh().await?; + } + Some(Action::Quit) => break, + None => {} + } + let dir = shell.env().pwd(); + if dir != prev_dir { + prev_dir = dir.to_path_buf(); + inputs.new_dir(dir.to_path_buf()); + } + } + + shell.history.save().await; + + Ok(0) +} + +#[derive(Copy, Clone, Debug)] +enum Focus { + Readline, + History(usize), + Scrolling(Option<usize>), +} + +#[derive(Copy, Clone, Debug)] +enum Scene { + Readline, + Fullscreen, +} + +pub enum Action { + Refresh, + HardRefresh, + Resize(u16, u16), + Quit, +} + +pub struct Shell { + readline: readline::Readline, + history: history::History, + old_history: old_history::History, + env: Env, + git: Option<inputs::GitInfo>, + focus: Focus, + scene: Scene, + escape: bool, + hide_readline: bool, + offset: time::UtcOffset, +} + +impl Shell { + pub fn new(offset: time::UtcOffset) -> Result<Self> { + let mut env = Env::new()?; + env.set_var("SHELL", std::env::current_exe()?); + env.set_var("TERM", "screen"); + Ok(Self { + readline: readline::Readline::new(), + history: history::History::new(), + old_history: old_history::History::new(), + env, + git: None, + focus: Focus::Readline, + scene: Scene::Readline, + escape: false, + hide_readline: false, + offset, + }) + } + + pub fn render(&self, out: &mut impl textmode::Textmode) -> Result<()> { + out.clear(); + out.write(&vt100::Parser::default().screen().input_mode_formatted()); + match self.scene { + Scene::Readline => match self.focus { + Focus::Readline => { + self.history.render( + out, + self.readline.lines(), + None, + false, + self.offset, + ); + self.readline.render( + out, + &self.env, + self.git.as_ref(), + true, + self.offset, + )?; + } + Focus::History(idx) => { + if self.hide_readline { + self.history.render( + out, + 0, + Some(idx), + false, + self.offset, + ); + } else { + self.history.render( + out, + self.readline.lines(), + Some(idx), + false, + self.offset, + ); + let pos = out.screen().cursor_position(); + self.readline.render( + out, + &self.env, + self.git.as_ref(), + false, + self.offset, + )?; + out.move_to(pos.0, pos.1); + } + } + Focus::Scrolling(idx) => { + self.history.render( + out, + self.readline.lines(), + idx, + true, + self.offset, + ); + self.readline.render( + out, + &self.env, + self.git.as_ref(), + idx.is_none(), + self.offset, + )?; + out.hide_cursor(true); + } + }, + Scene::Fullscreen => { + if let Focus::History(idx) = self.focus { + self.history.entry(idx).render_fullscreen(out); + } else { + unreachable!(); + } + } + } + Ok(()) + } + + pub fn handle_event( + &mut self, + event: Event, + event_w: &crate::shell::event::Writer, + ) -> Option<Action> { + match event { + Event::Key(key) => { + return if self.escape { + self.escape = false; + self.handle_key_escape(&key, event_w.clone()) + } else if key == textmode::Key::Ctrl(b'e') { + self.escape = true; + None + } else { + match self.focus { + Focus::Readline => { + self.handle_key_readline(&key, event_w.clone()) + } + Focus::History(idx) => { + self.handle_key_history(key, idx); + None + } + Focus::Scrolling(_) => { + self.handle_key_escape(&key, event_w.clone()) + } + } + }; + } + Event::Resize(new_size) => { + self.readline.resize(new_size); + self.history.resize(new_size); + return Some(Action::Resize(new_size.0, new_size.1)); + } + Event::PtyOutput => { + // the number of visible lines may have changed, so make sure + // the focus is still visible + self.history.make_focus_visible( + self.readline.lines(), + self.focus_idx(), + matches!(self.focus, Focus::Scrolling(_)), + ); + self.scene = self.default_scene(self.focus); + } + Event::ChildExit(idx, exit_info, env) => { + self.history.entry_mut(idx).exited(exit_info); + if self.focus_idx() == Some(idx) { + if let Some(env) = env { + if self.hide_readline { + let idx = self.env.idx(); + self.env = env; + self.env.set_idx(idx); + } + } + self.set_focus(if self.hide_readline { + Focus::Readline + } else { + Focus::Scrolling(Some(idx)) + }); + } + } + Event::ChildRunPipeline(idx, span) => { + self.history.entry_mut(idx).set_span(span); + } + Event::ChildSuspend(idx) => { + if self.focus_idx() == Some(idx) { + self.set_focus(Focus::Readline); + } + } + Event::GitInfo(info) => { + self.git = info; + } + Event::ClockTimer => {} + }; + Some(Action::Refresh) + } + + fn handle_key_escape( + &mut self, + key: &textmode::Key, + event_w: crate::shell::event::Writer, + ) -> Option<Action> { + match key { + textmode::Key::Ctrl(b'd') => { + return Some(Action::Quit); + } + textmode::Key::Ctrl(b'e') => { + self.set_focus(Focus::Scrolling(self.focus_idx())); + } + textmode::Key::Ctrl(b'l') => { + return Some(Action::HardRefresh); + } + textmode::Key::Ctrl(b'm') => { + if let Some(idx) = self.focus_idx() { + self.readline.clear_input(); + self.history.run( + self.history.entry(idx).cmd().to_string(), + self.env.clone(), + event_w, + ); + let idx = self.history.entry_count() - 1; + self.set_focus(Focus::History(idx)); + self.hide_readline = true; + self.env.set_idx(idx + 1); + } else { + self.set_focus(Focus::Readline); + } + } + textmode::Key::Char(' ') => { + if let Some(idx) = self.focus_idx() { + if self.history.entry(idx).running() { + self.set_focus(Focus::History(idx)); + } + } else { + self.set_focus(Focus::Readline); + } + } + textmode::Key::Char('e') => { + if let Focus::History(idx) = self.focus { + self.handle_key_history(textmode::Key::Ctrl(b'e'), idx); + } + } + textmode::Key::Char('f') => { + if let Some(idx) = self.focus_idx() { + let mut focus = Focus::History(idx); + let entry = self.history.entry_mut(idx); + if let Focus::Scrolling(_) = self.focus { + entry.set_fullscreen(true); + } else { + entry.toggle_fullscreen(); + if !entry.should_fullscreen() && !entry.running() { + focus = Focus::Scrolling(Some(idx)); + } + } + self.set_focus(focus); + } + } + textmode::Key::Char('i') => { + if let Some(idx) = self.focus_idx() { + self.readline + .set_input(self.history.entry(idx).cmd().to_string()); + self.set_focus(Focus::Readline); + } + } + textmode::Key::Char('j') | textmode::Key::Down => { + self.set_focus(Focus::Scrolling(self.scroll_down())); + } + textmode::Key::Char('k') | textmode::Key::Up => { + self.set_focus(Focus::Scrolling(self.scroll_up())); + } + textmode::Key::Char('n') => { + self.set_focus(self.next_running()); + } + textmode::Key::Char('p') => { + self.set_focus(self.prev_running()); + } + textmode::Key::Char('r') => { + self.set_focus(Focus::Readline); + } + _ => { + return None; + } + } + Some(Action::Refresh) + } + + fn handle_key_readline( + &mut self, + key: &textmode::Key, + event_w: crate::shell::event::Writer, + ) -> Option<Action> { + match key { + textmode::Key::Char(c) => { + self.readline.add_input(&c.to_string()); + } + textmode::Key::Ctrl(b'c') => self.readline.clear_input(), + textmode::Key::Ctrl(b'd') => { + return Some(Action::Quit); + } + textmode::Key::Ctrl(b'l') => { + return Some(Action::HardRefresh); + } + textmode::Key::Ctrl(b'm') => { + let input = self.readline.input(); + if !input.is_empty() { + self.history.run( + input.to_string(), + self.env.clone(), + event_w, + ); + let idx = self.history.entry_count() - 1; + self.set_focus(Focus::History(idx)); + self.hide_readline = true; + self.env.set_idx(idx + 1); + self.readline.clear_input(); + } + } + textmode::Key::Ctrl(b'u') => self.readline.clear_backwards(), + textmode::Key::Backspace => self.readline.backspace(), + textmode::Key::Left => self.readline.cursor_left(), + textmode::Key::Right => self.readline.cursor_right(), + textmode::Key::Up => { + let entry_count = self.history.entry_count(); + if entry_count > 0 { + self.set_focus(Focus::Scrolling(Some(entry_count - 1))); + } + } + _ => return None, + } + Some(Action::Refresh) + } + + fn handle_key_history(&mut self, key: textmode::Key, idx: usize) { + self.history.entry(idx).input(key.into_bytes()); + } + + fn default_scene(&self, focus: Focus) -> Scene { + match focus { + Focus::Readline | Focus::Scrolling(_) => Scene::Readline, + Focus::History(idx) => { + if self.history.entry(idx).should_fullscreen() { + Scene::Fullscreen + } else { + Scene::Readline + } + } + } + } + + fn set_focus(&mut self, new_focus: Focus) { + self.focus = new_focus; + self.hide_readline = false; + self.scene = self.default_scene(new_focus); + self.history.make_focus_visible( + self.readline.lines(), + self.focus_idx(), + matches!(self.focus, Focus::Scrolling(_)), + ); + } + + fn env(&self) -> &Env { + &self.env + } + + fn focus_idx(&self) -> Option<usize> { + match self.focus { + Focus::History(idx) => Some(idx), + Focus::Readline => None, + Focus::Scrolling(idx) => idx, + } + } + + fn scroll_up(&self) -> Option<usize> { + self.focus_idx().map_or_else( + || { + let count = self.history.entry_count(); + if count == 0 { + None + } else { + Some(count - 1) + } + }, + |idx| Some(idx.saturating_sub(1)), + ) + } + + fn scroll_down(&self) -> Option<usize> { + self.focus_idx().and_then(|idx| { + if idx >= self.history.entry_count() - 1 { + None + } else { + Some(idx + 1) + } + }) + } + + fn next_running(&self) -> Focus { + let count = self.history.entry_count(); + let cur = self.focus_idx().unwrap_or(count); + for idx in ((cur + 1)..count).chain(0..cur) { + if self.history.entry(idx).running() { + return Focus::History(idx); + } + } + self.focus + } + + fn prev_running(&self) -> Focus { + let count = self.history.entry_count(); + let cur = self.focus_idx().unwrap_or(count); + for idx in ((cur + 1)..count).chain(0..cur).rev() { + if self.history.entry(idx).running() { + return Focus::History(idx); + } + } + self.focus + } +} diff --git a/src/shell/old_history.rs b/src/shell/old_history.rs new file mode 100644 index 0000000..49fd1c2 --- /dev/null +++ b/src/shell/old_history.rs @@ -0,0 +1,185 @@ +use crate::shell::prelude::*; + +use tokio::io::AsyncBufReadExt as _; + +use pest::Parser as _; + +#[derive(pest_derive::Parser)] +#[grammar = "history.pest"] +struct HistoryLine; + +pub struct History { + entries: std::sync::Arc<std::sync::Mutex<Vec<Entry>>>, +} + +impl History { + pub fn new() -> Self { + let entries = std::sync::Arc::new(std::sync::Mutex::new(vec![])); + tokio::spawn(Self::task(std::sync::Arc::clone(&entries))); + Self { entries } + } + + pub fn entry_count(&self) -> usize { + self.entries.lock().unwrap().len() + } + + async fn task(entries: std::sync::Arc<std::sync::Mutex<Vec<Entry>>>) { + // TODO: we should actually read this in reverse order, because we + // want to populate the most recent entries first + let mut stream = tokio_stream::wrappers::LinesStream::new( + tokio::io::BufReader::new( + tokio::fs::File::open(crate::dirs::history_file()) + .await + .unwrap(), + ) + .lines(), + ); + while let Some(line) = stream.next().await { + let line = if let Ok(line) = line { + line + } else { + continue; + }; + let entry = if let Ok(entry) = line.parse() { + entry + } else { + continue; + }; + entries.lock().unwrap().push(entry); + } + } +} + +pub struct Entry { + cmdline: String, + start_time: Option<time::OffsetDateTime>, + duration: Option<std::time::Duration>, +} + +impl Entry { + pub fn render( + &self, + out: &mut impl textmode::Textmode, + offset: time::UtcOffset, + ) { + let size = out.screen().size(); + let mut time = "".to_string(); + if let Some(duration) = self.duration { + time.push_str(&crate::format::duration(duration)); + } + if let Some(start_time) = self.start_time { + time.push_str(&crate::format::time(start_time.to_offset(offset))); + } + + out.write_str(" $ "); + let start = usize::from(out.screen().cursor_position().1); + let end = usize::from(size.1) - time.len() - 2; + let max_len = end - start; + let cmd = if self.cmdline.len() > max_len { + &self.cmdline[..(max_len - 4)] + } else { + &self.cmdline + }; + out.write_str(cmd); + if self.cmdline.len() > max_len { + out.write_str(" "); + out.set_fgcolor(textmode::color::BLUE); + out.write_str("..."); + } + out.reset_attributes(); + + out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20)); + let cur_pos = out.screen().cursor_position(); + out.write_str(&" ".repeat( + usize::from(size.1) - time.len() - 1 - usize::from(cur_pos.1), + )); + out.write_str(&time); + out.write_str(" "); + out.reset_attributes(); + } + + pub fn cmd(&self) -> &str { + &self.cmdline + } +} + +impl std::str::FromStr for Entry { + type Err = anyhow::Error; + + fn from_str(line: &str) -> std::result::Result<Self, Self::Err> { + let mut parsed = + HistoryLine::parse(Rule::line, line).map_err(|e| anyhow!(e))?; + let line = parsed.next().unwrap(); + assert!(matches!(line.as_rule(), Rule::line)); + + let mut start_time = None; + let mut duration = None; + let mut cmdline = None; + for part in line.into_inner() { + match part.as_rule() { + Rule::time => { + start_time = + Some(time::OffsetDateTime::from_unix_timestamp( + part.as_str().parse()?, + )?); + } + Rule::duration => { + if part.as_str() == "0" { + continue; + } + let mut dur_parts = part.as_str().split('.'); + let secs: u64 = dur_parts.next().unwrap().parse()?; + let nsec_str = dur_parts.next().unwrap_or("0"); + let nsec_str = &nsec_str[..9.min(nsec_str.len())]; + let nsecs: u64 = nsec_str.parse()?; + duration = Some(std::time::Duration::from_nanos( + secs * 1_000_000_000 + + nsecs + * (10u64.pow( + (9 - nsec_str.len()).try_into().unwrap(), + )), + )); + } + Rule::command => { + cmdline = Some(part.as_str().to_string()); + } + Rule::line => unreachable!(), + Rule::EOI => break, + } + } + + Ok(Self { + cmdline: cmdline.unwrap(), + start_time, + duration, + }) + } +} + +#[test] +fn test_parse() { + let entry: Entry = + ": 1646779848:1234.56;vim ~/.zsh_history".parse().unwrap(); + assert_eq!(entry.cmdline, "vim ~/.zsh_history"); + assert_eq!( + entry.duration, + Some(std::time::Duration::from_nanos(1_234_560_000_000)) + ); + assert_eq!( + entry.start_time, + Some(time::macros::datetime!(2022-03-08 22:50:48).assume_utc()) + ); + + let entry: Entry = ": 1646779848:1;vim ~/.zsh_history".parse().unwrap(); + assert_eq!(entry.cmdline, "vim ~/.zsh_history"); + assert_eq!(entry.duration, Some(std::time::Duration::from_secs(1))); + assert_eq!( + entry.start_time, + Some(time::macros::datetime!(2022-03-08 22:50:48).assume_utc()) + ); + + let entry: Entry = "vim ~/.zsh_history".parse().unwrap(); + assert_eq!(entry.cmdline, "vim ~/.zsh_history"); + assert_eq!(entry.duration, None); + assert_eq!(entry.start_time, None); +} diff --git a/src/shell/prelude.rs b/src/shell/prelude.rs new file mode 100644 index 0000000..73897bc --- /dev/null +++ b/src/shell/prelude.rs @@ -0,0 +1,2 @@ +pub use super::event::Event; +pub use crate::prelude::*; diff --git a/src/shell/readline.rs b/src/shell/readline.rs new file mode 100644 index 0000000..654d264 --- /dev/null +++ b/src/shell/readline.rs @@ -0,0 +1,223 @@ +use crate::shell::prelude::*; + +use unicode_width::{UnicodeWidthChar as _, UnicodeWidthStr as _}; + +pub struct Readline { + size: (u16, u16), + input_line: String, + scroll: usize, + pos: usize, +} + +impl Readline { + pub fn new() -> Self { + Self { + size: (24, 80), + input_line: "".into(), + scroll: 0, + pos: 0, + } + } + + pub fn render( + &self, + out: &mut impl textmode::Textmode, + env: &Env, + git: Option<&super::inputs::GitInfo>, + focus: bool, + offset: time::UtcOffset, + ) -> Result<()> { + let pwd = env.pwd(); + let user = crate::info::user()?; + let hostname = crate::info::hostname()?; + let time = crate::info::time(offset)?; + let prompt_char = crate::info::prompt_char()?; + + let id = format!("{}@{}", user, hostname); + let idlen: u16 = id.len().try_into().unwrap(); + let timelen: u16 = time.len().try_into().unwrap(); + + out.move_to(self.size.0 - 2, 0); + if focus { + out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b)); + } else if env.idx() % 2 == 0 { + out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00)); + } else { + out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20)); + } + out.write(b"\x1b[K"); + out.set_fgcolor(textmode::color::YELLOW); + out.write_str(&format!("{}", env.idx() + 1)); + out.reset_attributes(); + if focus { + out.set_bgcolor(textmode::Color::Rgb(0x56, 0x1b, 0x8b)); + } else if env.idx() % 2 == 0 { + out.set_bgcolor(textmode::Color::Rgb(0x24, 0x21, 0x00)); + } else { + out.set_bgcolor(textmode::Color::Rgb(0x20, 0x20, 0x20)); + } + out.write_str(" ("); + out.write_str(&crate::format::path(pwd)); + if let Some(info) = git { + out.write_str(&format!("|{}", info)); + } + out.write_str(")"); + out.move_to(self.size.0 - 2, self.size.1 - 4 - idlen - timelen); + out.write_str(&id); + out.write_str(" ["); + out.write_str(&time); + out.write_str("]"); + + out.move_to(self.size.0 - 1, 0); + out.reset_attributes(); + out.write_str(&prompt_char); + out.write_str(" "); + out.reset_attributes(); + out.write(b"\x1b[K"); + out.write_str(self.visible_input()); + out.reset_attributes(); + out.move_to(self.size.0 - 1, 2 + self.pos_width()); + if focus { + out.hide_cursor(false); + } + Ok(()) + } + + pub fn resize(&mut self, size: (u16, u16)) { + self.size = size; + } + + // self will be used eventually + #[allow(clippy::unused_self)] + pub fn lines(&self) -> usize { + 2 // XXX handle wrapping + } + + pub fn input(&self) -> &str { + &self.input_line + } + + pub fn add_input(&mut self, s: &str) { + self.input_line.insert_str(self.byte_pos(), s); + self.inc_pos(s.chars().count()); + } + + pub fn set_input(&mut self, s: String) { + self.set_pos(s.chars().count()); + self.input_line = s; + } + + pub fn backspace(&mut self) { + while self.pos > 0 { + self.dec_pos(1); + let width = + self.input_line.remove(self.byte_pos()).width().unwrap_or(0); + if width > 0 { + break; + } + } + } + + pub fn clear_input(&mut self) { + self.input_line.clear(); + self.set_pos(0); + } + + pub fn clear_backwards(&mut self) { + self.input_line = self.input_line.chars().skip(self.pos).collect(); + self.set_pos(0); + } + + pub fn cursor_left(&mut self) { + if self.pos == 0 { + return; + } + self.dec_pos(1); + while let Some(c) = self.input_line.chars().nth(self.pos) { + if c.width().unwrap_or(0) == 0 { + self.dec_pos(1); + } else { + break; + } + } + } + + pub fn cursor_right(&mut self) { + if self.pos == self.input_line.chars().count() { + return; + } + self.inc_pos(1); + while let Some(c) = self.input_line.chars().nth(self.pos) { + if c.width().unwrap_or(0) == 0 { + self.inc_pos(1); + } else { + break; + } + } + } + + fn set_pos(&mut self, pos: usize) { + self.pos = pos; + if self.pos < self.scroll || self.pos_width() > self.size.1 - 2 { + self.scroll = self.pos; + let mut extra_scroll = usize::from(self.size.1) / 2; + while extra_scroll > 0 && self.scroll > 0 { + self.scroll -= 1; + extra_scroll -= self + .input_line + .chars() + .nth(self.scroll) + .unwrap() + .width() + .unwrap_or(1); + } + } + } + + fn inc_pos(&mut self, inc: usize) { + self.set_pos(self.pos + inc); + } + + fn dec_pos(&mut self, dec: usize) { + self.set_pos(self.pos - dec); + } + + fn pos_width(&self) -> u16 { + let start = self + .input_line + .char_indices() + .nth(self.scroll) + .map_or(self.input_line.len(), |(i, _)| i); + let end = self + .input_line + .char_indices() + .nth(self.pos) + .map_or(self.input_line.len(), |(i, _)| i); + self.input_line[start..end].width().try_into().unwrap() + } + + fn byte_pos(&self) -> usize { + self.input_line + .char_indices() + .nth(self.pos) + .map_or(self.input_line.len(), |(i, _)| i) + } + + fn visible_input(&self) -> &str { + let start = self + .input_line + .char_indices() + .nth(self.scroll) + .map_or(self.input_line.len(), |(i, _)| i); + let mut end = self.input_line.len(); + let mut width = 0; + for (i, c) in self.input_line.char_indices().skip(self.scroll) { + if width >= usize::from(self.size.1) - 2 { + end = i; + break; + } + width += c.width().unwrap_or(1); + } + &self.input_line[start..end] + } +} diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index c348d7f..0000000 --- a/src/state.rs +++ /dev/null @@ -1,181 +0,0 @@ -use textmode::Textmode as _; - -pub struct State { - readline: crate::readline::Readline, - history: crate::history::History, - focus: Focus, - escape: bool, - hide_readline: bool, -} - -impl State { - pub fn new() -> Self { - let readline = crate::readline::Readline::new(); - let history = crate::history::History::new(); - let focus = Focus::Readline; - Self { - readline, - history, - focus, - escape: false, - hide_readline: false, - } - } - - pub async fn handle_key( - &mut self, - key: textmode::Key, - ) -> Option<crate::action::Action> { - if self.escape { - self.escape = false; - let mut fallthrough = false; - match key { - textmode::Key::Ctrl(b'e') => { - fallthrough = true; - } - textmode::Key::Ctrl(b'l') => { - return Some(crate::action::Action::ForceRedraw); - } - textmode::Key::Char('f') => { - if let Focus::History(idx) = self.focus { - return Some( - crate::action::Action::ToggleFullscreen(idx), - ); - } - } - textmode::Key::Char('j') => { - let new_focus = match self.focus { - Focus::History(idx) => { - if idx >= self.history.entry_count() - 1 { - Focus::Readline - } else { - Focus::History(idx + 1) - } - } - Focus::Readline => Focus::Readline, - }; - return Some(crate::action::Action::UpdateFocus( - new_focus, - )); - } - textmode::Key::Char('k') => { - let new_focus = match self.focus { - Focus::History(idx) => { - if idx == 0 { - Focus::History(0) - } else { - Focus::History(idx - 1) - } - } - Focus::Readline => { - Focus::History(self.history.entry_count() - 1) - } - }; - return Some(crate::action::Action::UpdateFocus( - new_focus, - )); - } - textmode::Key::Char('r') => { - return Some(crate::action::Action::UpdateFocus( - Focus::Readline, - )); - } - _ => {} - } - if !fallthrough { - return None; - } - } else if key == textmode::Key::Ctrl(b'e') { - self.escape = true; - return None; - } - - match self.focus { - Focus::Readline => self.readline.handle_key(key).await, - Focus::History(idx) => { - self.history.handle_key(key, idx).await; - None - } - } - } - - pub async fn render( - &self, - out: &mut textmode::Output, - hard: bool, - ) -> anyhow::Result<()> { - out.clear(); - match self.focus { - Focus::Readline => { - self.history - .render(out, self.readline.lines(), None) - .await?; - self.readline.render(out, true).await?; - } - Focus::History(idx) => { - if self.hide_readline || self.history.is_fullscreen(idx).await - { - self.history.render(out, 0, Some(idx)).await?; - } else { - self.history - .render(out, self.readline.lines(), Some(idx)) - .await?; - let pos = out.screen().cursor_position(); - self.readline.render(out, false).await?; - out.move_to(pos.0, pos.1); - } - } - } - if hard { - out.hard_refresh().await?; - } else { - out.refresh().await?; - } - Ok(()) - } - - pub async fn handle_action( - &mut self, - action: crate::action::Action, - out: &mut textmode::Output, - action_w: &async_std::channel::Sender<crate::action::Action>, - ) { - let mut hard_refresh = false; - match action { - crate::action::Action::Render => {} - crate::action::Action::ForceRedraw => { - hard_refresh = true; - } - crate::action::Action::Run(ref cmd) => { - let idx = - self.history.run(cmd, action_w.clone()).await.unwrap(); - self.focus = Focus::History(idx); - self.hide_readline = true; - } - crate::action::Action::UpdateFocus(new_focus) => { - self.focus = new_focus; - self.hide_readline = false; - } - crate::action::Action::ToggleFullscreen(idx) => { - self.history.toggle_fullscreen(idx).await; - } - crate::action::Action::Resize(new_size) => { - self.readline.resize(new_size).await; - self.history.resize(new_size).await; - out.set_size(new_size.0, new_size.1); - out.hard_refresh().await.unwrap(); - } - crate::action::Action::Quit => { - // the debouncer should return None in this case - unreachable!(); - } - } - self.render(out, hard_refresh).await.unwrap(); - } -} - -#[derive(Copy, Clone, Debug)] -pub enum Focus { - Readline, - History(usize), -} diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index d792b91..0000000 --- a/src/util.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub type Mutex<T> = async_std::sync::Arc<async_std::sync::Mutex<T>>; - -pub fn mutex<T>(t: T) -> Mutex<T> { - async_std::sync::Arc::new(async_std::sync::Mutex::new(t)) -} |