summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1780
-rw-r--r--Cargo.toml46
-rw-r--r--Makefile23
-rw-r--r--deny.toml2
-rw-r--r--src/action.rs123
-rw-r--r--src/builtins.rs44
-rw-r--r--src/config.rs25
-rw-r--r--src/dirs.rs20
-rw-r--r--src/env.rs151
-rw-r--r--src/format.rs51
-rw-r--r--src/history.pest5
-rw-r--r--src/history.rs407
-rw-r--r--src/info.rs67
-rw-r--r--src/main.rs137
-rw-r--r--src/parse.rs8
-rw-r--r--src/parse/ast.rs600
-rw-r--r--src/parse/mod.rs169
-rw-r--r--src/parse/test_ast.rs507
-rw-r--r--src/prelude.rs51
-rw-r--r--src/readline.rs198
-rw-r--r--src/runner/builtins/command.rs373
-rw-r--r--src/runner/builtins/mod.rs242
-rw-r--r--src/runner/command.rs203
-rw-r--r--src/runner/mod.rs499
-rw-r--r--src/runner/prelude.rs1
-rw-r--r--src/runner/sys.rs79
-rw-r--r--src/shell.pest72
-rw-r--r--src/shell/event.rs163
-rw-r--r--src/shell/history/entry.rs429
-rw-r--r--src/shell/history/mod.rs208
-rw-r--r--src/shell/history/pty.rs196
-rw-r--r--src/shell/inputs/clock.rs27
-rw-r--r--src/shell/inputs/git.rs274
-rw-r--r--src/shell/inputs/mod.rs32
-rw-r--r--src/shell/inputs/signals.rs30
-rw-r--r--src/shell/inputs/stdin.rs17
-rw-r--r--src/shell/mod.rs484
-rw-r--r--src/shell/old_history.rs185
-rw-r--r--src/shell/prelude.rs2
-rw-r--r--src/shell/readline.rs223
-rw-r--r--src/state.rs181
-rw-r--r--src/util.rs5
42 files changed, 6932 insertions, 1407 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e47ce2c..dbd3a93 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 38ac03e..89b201e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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
diff --git a/deny.toml b/deny.toml
index 5b7ebc5..90446c7 100644
--- a/deny.toml
+++ b/deny.toml
@@ -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))
-}