diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcadb2c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..997b6fe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +on: + push: + branches: + - main + pull_request: + branches: + - main + +name: CI + +jobs: + clippy_check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v1 + with: + submodules: true + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: clippy + override: true + - name: Check Clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + toolchain: stable + args: + test: + strategy: + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + submodules: true + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: rustfmt, clippy + - name: Cache cargo registry + uses: actions/cache@v1 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - name: Run tests + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/.gitignore b/.gitignore index ea8c4bf..74269e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/TODO diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..e9ad996 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,12 @@ +unstable_features = true +max_width = 100 +fn_call_width = 90 +struct_lit_width = 50 +struct_variant_width = 50 +chain_width = 75 +binop_separator = "Back" +force_multiline_blocks = true +match_block_trailing_comma = true +imports_layout = "HorizontalVertical" +newline_style = "Unix" +overflow_delimited_expr = true diff --git a/Cargo.lock b/Cargo.lock index ea3543c..6272eed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,3172 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", + "rand_core 0.6.4", +] + +[[package]] +name = "aes" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +dependencies = [ + "cfg-if", + "cipher 0.4.3", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7724808837b77f4b4de9d283820f9d98bcf496d5692934b857a2399d31ff22e6" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +dependencies = [ + "serde", +] + +[[package]] +name = "assign" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-once-cell" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72faff1fdc615a0199d7bf71e6f389af54d46a66e9beb5d76c39e48eda93ecce" + +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d1d8ab452a3936018a687b20e6f7cf5363d713b732b8884001317b0e48aa3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.8", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake3" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.6", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.3", +] + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6" +dependencies = [ + "cfg-if", + "cipher 0.3.0", + "cpufeatures", + "zeroize", +] + +[[package]] +name = "chacha20poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18446b09be63d457bbec447509e85f662f32952b035ce892290396bc0b0cff5" +dependencies = [ + "aead", + "chacha20", + "cipher 0.3.0", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cipher" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "constant_time_eq" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot 0.12.1", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.3", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "cxx" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27874566aca772cb515af4c6e997b5fe2119820bca447689145e39bb734d19a0" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb951f2523a49533003656a72121306b225ec16a49a09dc6b0ba0d6f3ec3c0" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be778b6327031c1c7b61dd2e48124eee5361e6aa76b8de93692f011b08870ab4" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b8a2b87662fe5a0a0b38507756ab66aff32638876a0866e5a5fc82ceb07ee49" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core 0.9.5", +] + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid", +] + +[[package]] +name = "derive_builder" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer 0.10.3", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "serde_bytes", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "futures" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" + +[[package]] +name = "futures-executor" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-signals" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3acc659ba666cff13fdf65242d16428f2f11935b688f82e4024ad39667a5132" +dependencies = [ + "discard", + "futures-channel", + "futures-core", + "futures-util", + "pin-project", +] + +[[package]] +name = "futures-sink" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" + +[[package]] +name = "futures-task" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" + +[[package]] +name = "futures-util" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a329e22866dd78b35d2c639a4a23d7b950aeae300dfd79f4fb19f74055c2404" +dependencies = [ + "libc", + "windows", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[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" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +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-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "iamb" version = "0.0.1" +dependencies = [ + "chrono", + "clap", + "dirs", + "futures", + "gethostname", + "lazy_static", + "matrix-sdk", + "modalkit", + "regex", + "rpassword", + "serde", + "serde_json", + "sled", + "thiserror", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", + "unicode-segmentation", + "unicode-width", + "url", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexed_db_futures" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26ac735f676c52305becf53264b91cea9866a8de61ccbf464405b377b9cbca9" +dependencies = [ + "cfg-if", + "js-sys", + "uuid 0.8.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "intervaltree" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "270bc34e57047cab801a8c871c124d9dc7132f6473c6401f645524f4e6edd111" +dependencies = [ + "smallvec", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "ipnet" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11b0d96e660696543b251e58030cf9787df56da39dab19ad60eae7353040917e" + +[[package]] +name = "is-terminal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "js_int" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a" +dependencies = [ + "serde", +] + +[[package]] +name = "js_option" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c" +dependencies = [ + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lru" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e8aaa3f231bb4bd57b84b2d5dc3ae7f350265df8aa96492e0bc394a1571909" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matrix-sdk" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbeafb4809f33f377165f2fbcf10e0613053ad206762194c3050a727fd3abcb2" +dependencies = [ + "anymap2", + "async-once-cell", + "async-stream", + "async-trait", + "backoff", + "bytes", + "dashmap", + "derive_builder", + "event-listener", + "futures-core", + "futures-signals", + "futures-util", + "http", + "matrix-sdk-base", + "matrix-sdk-common", + "matrix-sdk-indexeddb", + "matrix-sdk-sled", + "mime", + "reqwest", + "ruma", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "url", + "wasm-timer", + "zeroize", +] + +[[package]] +name = "matrix-sdk-base" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b944f6d1fc8779ba790dd0b942ceff45c626c1f5da847f01122d355ad06511bd" +dependencies = [ + "async-stream", + "async-trait", + "dashmap", + "futures-channel", + "futures-core", + "futures-signals", + "futures-util", + "lru", + "matrix-sdk-common", + "matrix-sdk-crypto", + "once_cell", + "ruma", + "serde", + "serde_json", + "thiserror", + "tracing", + "zeroize", +] + +[[package]] +name = "matrix-sdk-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85a6a743cc9dcf9385e61a26db78276078beddd27f3762d9d82baa2030695f1" +dependencies = [ + "async-lock", + "futures-core", + "futures-util", + "instant", + "ruma", + "serde", + "tokio", + "wasm-bindgen-futures", + "wasm-timer", +] + +[[package]] +name = "matrix-sdk-crypto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fa699e8dd54578a4b92e3fcd18a50da8e415a0c042da1706b0330fc2d8f949" +dependencies = [ + "aes", + "async-trait", + "atomic", + "base64", + "byteorder", + "ctr", + "dashmap", + "event-listener", + "futures-util", + "hmac", + "matrix-sdk-common", + "pbkdf2", + "rand 0.8.5", + "ruma", + "serde", + "serde_json", + "sha2 0.10.6", + "thiserror", + "tokio", + "tracing", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-indexeddb" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7847d36bba832bc787214323bc042b71dca7fdf2aee9f0e3eb573b64f2f7eb7f" +dependencies = [ + "anyhow", + "async-trait", + "base64", + "dashmap", + "derive_builder", + "getrandom 0.2.8", + "indexed_db_futures", + "js-sys", + "matrix-sdk-base", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "ruma", + "serde", + "serde_json", + "thiserror", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "matrix-sdk-sled" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ded5a703ad8a82b8edfde808228711315c8761a5fbf7ac2b98ab4951dadd066" +dependencies = [ + "async-stream", + "async-trait", + "dashmap", + "derive_builder", + "fs_extra", + "futures-core", + "futures-util", + "matrix-sdk-base", + "matrix-sdk-common", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "ruma", + "serde", + "serde_json", + "sled", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "matrix-sdk-store-encryption" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ddee75c3cca58f3a323283dc4e849d19d52988903f907ed0fb53dcad5d6fd25" +dependencies = [ + "blake3", + "chacha20poly1305", + "displaydoc", + "hmac", + "pbkdf2", + "rand 0.8.5", + "serde", + "serde_json", + "sha2 0.10.6", + "thiserror", + "zeroize", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "modalkit" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4385b7136847cd063fbf093f0aa21a098398e26dc9213137fdd4e4f593d6bf" +dependencies = [ + "anymap2", + "bitflags", + "crossterm", + "derive_more", + "intervaltree", + "libc", + "nom", + "regex", + "ropey", + "thiserror", + "tui", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi 0.1.19", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.5", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.6", + "hmac", + "password-hash", + "sha2 0.10.6", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs7" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7364e6d0e236473de91e042395d71e0e64715f99a60620b014a4a4c7d1619b" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048aeb476be11a4b6ca432ca569e375810de9294ae78f4774e78ea98a9246ede" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +dependencies = [ + "once_cell", + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "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.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d89e5dba24725ae5678020bf8f1357a9aa7ff10736b551adbcd3f8d17d766f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b18e655c21ff5ac2084a5ad0611e827b3f92badf79f4910b5a5c58f4d87ff0" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164ae68b6587001ca506d3bf7f1000bfa248d0e1217b618108fba4ec1d0cc306" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556d0f47a940e895261e77dc200d5eadfc6ef644c179c6f5edfc105e3a2292c8" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.8", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "reqwest" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "ropey" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd22239fafefc42138ca5da064f3c17726a80d2379d817a3521240e78dd0064" +dependencies = [ + "smallvec", + "str_indices", +] + +[[package]] +name = "rpassword" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "ruma" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc348e3a4a18abc4e97fffa5e2e623f6edd50ba3a1dd5f47eb249fea713b69f" +dependencies = [ + "assign", + "js_int", + "js_option", + "ruma-client-api", + "ruma-common", + "ruma-federation-api", +] + +[[package]] +name = "ruma-client-api" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e72bc731b4dc8b569aa83915f13e419144b67110d858c65bb74aa05e2dc4b7" +dependencies = [ + "assign", + "bytes", + "http", + "js_int", + "maplit", + "percent-encoding", + "ruma-common", + "serde", + "serde_json", +] + +[[package]] +name = "ruma-common" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "716889595f4edc3cfeb94d9f122e413f73e37d7d80ea1c14196e1004241a3889" +dependencies = [ + "base64", + "bytes", + "form_urlencoded", + "getrandom 0.2.8", + "http", + "indexmap", + "itoa", + "js-sys", + "js_int", + "js_option", + "percent-encoding", + "rand 0.8.5", + "regex", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "uuid 1.2.2", + "wildmatch", +] + +[[package]] +name = "ruma-federation-api" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f905d12f6144c7a754bd0339fa6893698c03d03a908abb20cc6eeb4ec7f9466" +dependencies = [ + "js_int", + "ruma-common", + "serde", + "serde_json", +] + +[[package]] +name = "ruma-identifiers-validation" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabac62d16465a87435579c779d74dceabb93b09e44c766af6085050f3cc4275" +dependencies = [ + "js_int", + "thiserror", +] + +[[package]] +name = "ruma-macros" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f82e91eb61cd86d9287303133ee55b54618eccb75a522cc22a42c15f5bda340" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "ruma-identifiers-validation", + "serde", + "syn", + "toml", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bfa246f936730408c0abee392cc1a50b118ece708c7f630516defd64480c7d8" + +[[package]] +name = "serde" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718dc5fff5b36f99093fc49b280cfc96ce6fc824317783bff5a1fed0c7a64819" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8778cc0b528968fe72abec38b5db5a20a70d148116cd9325d2bc5f5180ca3faf" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "der", +] + +[[package]] +name = "str_indices" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ee3a69cd2c7e06684677e5629b3878b253af05e4714964204279c6bc02cf0b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +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.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" +dependencies = [ + "crossbeam-channel", + "time 0.3.17", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "tui" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1" +dependencies = [ + "bitflags", + "cassowary", + "crossterm", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +dependencies = [ + "getrandom 0.2.8", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vodozemac" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f20153a1c82ac5f1243b62e80f067ae608facc415c6ef82f88426a61c79886" +dependencies = [ + "aes", + "arrayvec", + "base64", + "cbc", + "ed25519-dalek", + "hkdf", + "hmac", + "pkcs7", + "prost", + "rand 0.7.3", + "serde", + "serde_json", + "sha2 0.10.6", + "subtle", + "thiserror", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "serde", + "serde_json", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "wildmatch" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "x25519-dalek" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "serde", + "zeroize", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml index 6b94d4f..35b1237 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,38 @@ [package] name = "iamb" version = "0.0.1" +edition = "2018" authors = ["Ulyssa "] repository = "https://github.com/ulyssa/iamb" homepage = "http://iamb.chat" readme = "README.md" -description = "A Matrix chat client with vim-style editing and keybindings" +description = "A Matrix chat client that uses Vim keybindings" license = "Apache-2.0" -edition = "2018" exclude = [".github", "CONTRIBUTING.md"] +keywords = ["matrix", "chat", "tui", "vim"] +rust-version = "1.65" [dependencies] +chrono = "0.4" +clap = {version = "4.0", features = ["derive"]} +dirs = "4.0.0" +futures = "0.3.21" +gethostname = "0.4.1" +matrix-sdk = {version = "0.6", default-features = false, features = ["e2e-encryption", "sled", "rustls-tls"]} +modalkit = "0.0.7" +regex = "^1.5" +rpassword = "^7.2" +serde = "^1.0" +serde_json = "^1.0" +sled = "0.34" +thiserror = "^1.0.37" +tokio = {version = "1.17.0", features = ["full"]} +tracing = "~0.1.36" +tracing-appender = "~0.2.2" +tracing-subscriber = "0.3.16" +unicode-segmentation = "^1.7" +unicode-width = "0.1.10" +url = {version = "^2.2.2", features = ["serde"]} + +[dev-dependencies] +lazy_static = "1.4.0" diff --git a/README.md b/README.md index 8b32237..d85869a 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ ## About -This is a vi-inspired terminal chat client for the Matrix protocol. +`iamb` is a Matrix client for the terminal that uses Vim keybindings. -__*Note that this project is still very much in its early stages and a -lot is subject to eventually change.*__ +This project is a work-in-progress, and there's still a lot to be implemented, +but much of the basic client functionality is already present. ## Installation @@ -15,6 +15,21 @@ Install Rust and Cargo, and then run: cargo install iamb ``` +## Configuration + +You can create a basic configuration in `$CONFIG_DIR/iamb/config.json` that looks like: + +```json +{ + "profiles": { + "example.com": { + "url": "https://example.com", + "@user:example.com" + } + } +} +``` + ## License iamb is released under the [Apache License, Version 2.0]. diff --git a/src/base.rs b/src/base.rs new file mode 100644 index 0000000..4857d04 --- /dev/null +++ b/src/base.rs @@ -0,0 +1,328 @@ +use std::collections::{HashMap, HashSet}; +use std::hash::Hash; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use tokio::sync::Mutex as AsyncMutex; +use tracing::warn; + +use matrix_sdk::{ + encryption::verification::SasVerification, + ruma::{OwnedRoomId, RoomId}, +}; + +use modalkit::{ + editing::{ + action::{Action, UIError, UIResult}, + application::{ + ApplicationAction, + ApplicationContentId, + ApplicationError, + ApplicationInfo, + ApplicationStore, + ApplicationWindowId, + }, + context::EditContext, + store::Store, + }, + env::vim::{ + command::{VimCommand, VimCommandMachine}, + keybindings::VimMachine, + VimContext, + }, + input::bindings::SequenceStatus, + input::key::TerminalKey, +}; + +use crate::{ + message::{Message, Messages}, + worker::Requester, + ApplicationSettings, +}; + +const ROOM_FETCH_DEBOUNCE: Duration = Duration::from_secs(3); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum IambInfo {} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum VerifyAction { + Accept, + Cancel, + Confirm, + Mismatch, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum IambAction { + Verify(VerifyAction, String), + VerifyRequest(String), + SendMessage(OwnedRoomId, String), + ToggleScrollbackFocus, +} + +impl ApplicationAction for IambAction { + fn is_edit_sequence(&self, _: &C) -> SequenceStatus { + match self { + IambAction::SendMessage(..) => SequenceStatus::Break, + IambAction::ToggleScrollbackFocus => SequenceStatus::Break, + IambAction::Verify(..) => SequenceStatus::Break, + IambAction::VerifyRequest(..) => SequenceStatus::Break, + } + } + + fn is_last_action(&self, _: &C) -> SequenceStatus { + match self { + IambAction::SendMessage(..) => SequenceStatus::Atom, + IambAction::ToggleScrollbackFocus => SequenceStatus::Atom, + IambAction::Verify(..) => SequenceStatus::Atom, + IambAction::VerifyRequest(..) => SequenceStatus::Atom, + } + } + + fn is_last_selection(&self, _: &C) -> SequenceStatus { + match self { + IambAction::SendMessage(..) => SequenceStatus::Ignore, + IambAction::ToggleScrollbackFocus => SequenceStatus::Ignore, + IambAction::Verify(..) => SequenceStatus::Ignore, + IambAction::VerifyRequest(..) => SequenceStatus::Ignore, + } + } + + fn is_switchable(&self, _: &C) -> bool { + match self { + IambAction::SendMessage(..) => false, + IambAction::ToggleScrollbackFocus => false, + IambAction::Verify(..) => false, + IambAction::VerifyRequest(..) => false, + } + } +} + +impl From for ProgramAction { + fn from(act: IambAction) -> Self { + Action::Application(act) + } +} + +pub type ProgramAction = Action; +pub type ProgramContext = VimContext; +pub type Keybindings = VimMachine; +pub type ProgramCommand = VimCommand; +pub type ProgramCommands = VimCommandMachine; +pub type ProgramStore = Store; +pub type AsyncProgramStore = Arc>; + +pub type IambResult = UIResult; + +#[derive(thiserror::Error, Debug)] +pub enum IambError { + #[error("Unknown room identifier: {0}")] + InvalidUserId(String), + + #[error("Invalid verification user/device pair: {0}")] + InvalidVerificationId(String), + + #[error("Cryptographic storage error: {0}")] + CryptoStore(#[from] matrix_sdk::encryption::CryptoStoreError), + + #[error("HTTP client error: {0}")] + Http(#[from] matrix_sdk::HttpError), + + #[error("Matrix client error: {0}")] + Matrix(#[from] matrix_sdk::Error), + + #[error("Matrix client storage error: {0}")] + Store(#[from] matrix_sdk::StoreError), + + #[error("Serialization/deserialization error: {0}")] + Serde(#[from] serde_json::Error), + + #[error("Unknown room identifier: {0}")] + UnknownRoom(OwnedRoomId), + + #[error("Verification request error: {0}")] + VerificationRequestError(#[from] matrix_sdk::encryption::identities::RequestVerificationError), +} + +impl From for UIError { + fn from(err: IambError) -> Self { + UIError::Application(err) + } +} + +impl ApplicationError for IambError {} + +#[derive(Default)] +pub enum RoomFetchStatus { + Done, + HaveMore(String), + #[default] + NotStarted, +} + +#[derive(Default)] +pub struct RoomInfo { + pub name: Option, + pub messages: Messages, + pub fetch_id: RoomFetchStatus, + pub fetch_last: Option, +} + +impl RoomInfo { + fn recently_fetched(&self) -> bool { + self.fetch_last.map_or(false, |i| i.elapsed() < ROOM_FETCH_DEBOUNCE) + } +} + +pub struct ChatStore { + pub worker: Requester, + pub rooms: HashMap, + pub names: HashMap, + pub verifications: HashMap, + pub settings: ApplicationSettings, + pub need_load: HashSet, +} + +impl ChatStore { + pub fn new(worker: Requester, settings: ApplicationSettings) -> Self { + ChatStore { + worker, + settings, + + names: Default::default(), + rooms: Default::default(), + verifications: Default::default(), + need_load: Default::default(), + } + } + + pub fn mark_for_load(&mut self, room_id: OwnedRoomId) { + self.need_load.insert(room_id); + } + + pub fn load_older(&mut self, limit: u32) { + let ChatStore { need_load, rooms, worker, .. } = self; + + for room_id in std::mem::take(need_load).into_iter() { + let info = rooms.entry(room_id.clone()).or_default(); + + if info.recently_fetched() { + need_load.insert(room_id); + continue; + } else { + info.fetch_last = Instant::now().into(); + } + + let fetch_id = match &info.fetch_id { + RoomFetchStatus::Done => continue, + RoomFetchStatus::HaveMore(fetch_id) => Some(fetch_id.clone()), + RoomFetchStatus::NotStarted => None, + }; + + let res = worker.load_older(room_id.clone(), fetch_id, limit); + + match res { + Ok((fetch_id, msgs)) => { + for msg in msgs.into_iter() { + let key = (msg.origin_server_ts().into(), msg.event_id().to_owned()); + + info.messages.insert(key, Message::from(msg)); + } + + info.fetch_id = + fetch_id.map_or(RoomFetchStatus::Done, RoomFetchStatus::HaveMore); + }, + Err(e) => { + warn!( + room_id = room_id.as_str(), + err = e.to_string(), + "Failed to load older messages" + ); + + // Wait and try again. + need_load.insert(room_id); + }, + } + } + } + + pub fn get_room_info(&mut self, room_id: OwnedRoomId) -> &mut RoomInfo { + self.rooms.entry(room_id).or_default() + } + + pub fn set_room_name(&mut self, room_id: &RoomId, name: &str) { + self.rooms.entry(room_id.to_owned()).or_default().name = name.to_string().into(); + } + + pub fn insert_sas(&mut self, sas: SasVerification) { + let key = format!("{}/{}", sas.other_user_id(), sas.other_device().device_id()); + + self.verifications.insert(key, sas); + } +} + +impl ApplicationStore for ChatStore {} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum IambId { + Room(OwnedRoomId), + DirectList, + RoomList, + SpaceList, + VerifyList, + Welcome, +} + +impl ApplicationWindowId for IambId {} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum RoomFocus { + Scrollback, + MessageBar, +} + +impl RoomFocus { + pub fn is_scrollback(&self) -> bool { + matches!(self, RoomFocus::Scrollback) + } + + pub fn is_msgbar(&self) -> bool { + matches!(self, RoomFocus::MessageBar) + } +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum IambBufferId { + Command, + Room(OwnedRoomId, RoomFocus), + DirectList, + RoomList, + SpaceList, + VerifyList, + Welcome, +} + +impl IambBufferId { + pub fn to_window(&self) -> Option { + match self { + IambBufferId::Command => None, + IambBufferId::Room(room, _) => Some(IambId::Room(room.clone())), + IambBufferId::DirectList => Some(IambId::DirectList), + IambBufferId::RoomList => Some(IambId::RoomList), + IambBufferId::SpaceList => Some(IambId::SpaceList), + IambBufferId::VerifyList => Some(IambId::VerifyList), + IambBufferId::Welcome => Some(IambId::Welcome), + } + } +} + +impl ApplicationContentId for IambBufferId {} + +impl ApplicationInfo for IambInfo { + type Error = IambError; + type Store = ChatStore; + type Action = IambAction; + type WindowId = IambId; + type ContentId = IambBufferId; +} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..b50a3b2 --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,202 @@ +use modalkit::{ + editing::{action::WindowAction, base::OpenTarget}, + env::vim::command::{CommandContext, CommandDescription}, + input::commands::{CommandError, CommandResult, CommandStep}, + input::InputContext, +}; + +use crate::base::{ + IambAction, + IambId, + ProgramCommand, + ProgramCommands, + ProgramContext, + VerifyAction, +}; + +type ProgContext = CommandContext; +type ProgResult = CommandResult; + +fn iamb_verify(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + let mut args = desc.arg.strings()?; + + match args.len() { + 0 => { + let open = WindowAction::Switch(OpenTarget::Application(IambId::VerifyList)); + let step = CommandStep::Continue(open.into(), ctx.context.take()); + + return Ok(step); + }, + 1 => { + return Result::Err(CommandError::InvalidArgument); + }, + 2 => { + let act = match args[0].as_str() { + "accept" => VerifyAction::Accept, + "cancel" => VerifyAction::Cancel, + "confirm" => VerifyAction::Confirm, + "mismatch" => VerifyAction::Mismatch, + "request" => { + let iact = IambAction::VerifyRequest(args.remove(1)); + let step = CommandStep::Continue(iact.into(), ctx.context.take()); + + return Ok(step); + }, + _ => return Result::Err(CommandError::InvalidArgument), + }; + + let vact = IambAction::Verify(act, args.remove(1)); + let step = CommandStep::Continue(vact.into(), ctx.context.take()); + + return Ok(step); + }, + _ => { + return Result::Err(CommandError::InvalidArgument); + }, + } +} + +fn iamb_dms(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let open = WindowAction::Switch(OpenTarget::Application(IambId::DirectList)); + let step = CommandStep::Continue(open.into(), ctx.context.take()); + + return Ok(step); +} + +fn iamb_rooms(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let open = WindowAction::Switch(OpenTarget::Application(IambId::RoomList)); + let step = CommandStep::Continue(open.into(), ctx.context.take()); + + return Ok(step); +} + +fn iamb_spaces(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let open = WindowAction::Switch(OpenTarget::Application(IambId::SpaceList)); + let step = CommandStep::Continue(open.into(), ctx.context.take()); + + return Ok(step); +} + +fn iamb_welcome(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + if !desc.arg.text.is_empty() { + return Result::Err(CommandError::InvalidArgument); + } + + let open = WindowAction::Switch(OpenTarget::Application(IambId::Welcome)); + let step = CommandStep::Continue(open.into(), ctx.context.take()); + + return Ok(step); +} + +fn iamb_join(desc: CommandDescription, ctx: &mut ProgContext) -> ProgResult { + let mut args = desc.arg.filenames()?; + + if args.len() != 1 { + return Result::Err(CommandError::InvalidArgument); + } + + let open = WindowAction::Switch(args.remove(0)); + let step = CommandStep::Continue(open.into(), ctx.context.take()); + + return Ok(step); +} + +fn add_iamb_commands(cmds: &mut ProgramCommands) { + cmds.add_command(ProgramCommand { names: vec!["dms".into()], f: iamb_dms }); + cmds.add_command(ProgramCommand { names: vec!["join".into()], f: iamb_join }); + cmds.add_command(ProgramCommand { names: vec!["rooms".into()], f: iamb_rooms }); + cmds.add_command(ProgramCommand { names: vec!["spaces".into()], f: iamb_spaces }); + cmds.add_command(ProgramCommand { names: vec!["verify".into()], f: iamb_verify }); + cmds.add_command(ProgramCommand { names: vec!["welcome".into()], f: iamb_welcome }); +} + +pub fn setup_commands() -> ProgramCommands { + let mut cmds = ProgramCommands::default(); + + add_iamb_commands(&mut cmds); + + return cmds; +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_cmd_verify() { + let mut cmds = setup_commands(); + let ctx = ProgramContext::default(); + + let res = cmds.input_cmd(":verify", ctx.clone()).unwrap(); + let act = WindowAction::Switch(OpenTarget::Application(IambId::VerifyList)); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd(":verify request @user1:example.com", ctx.clone()).unwrap(); + let act = IambAction::VerifyRequest("@user1:example.com".into()); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds + .input_cmd(":verify accept @user1:example.com/FOOBAR", ctx.clone()) + .unwrap(); + let act = IambAction::Verify(VerifyAction::Accept, "@user1:example.com/FOOBAR".into()); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds + .input_cmd(":verify mismatch @user2:example.com/QUUXBAZ", ctx.clone()) + .unwrap(); + let act = IambAction::Verify(VerifyAction::Mismatch, "@user2:example.com/QUUXBAZ".into()); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds + .input_cmd(":verify cancel @user3:example.com/MYDEVICE", ctx.clone()) + .unwrap(); + let act = IambAction::Verify(VerifyAction::Cancel, "@user3:example.com/MYDEVICE".into()); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds + .input_cmd(":verify confirm @user4:example.com/GOODDEV", ctx.clone()) + .unwrap(); + let act = IambAction::Verify(VerifyAction::Confirm, "@user4:example.com/GOODDEV".into()); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd(":verify confirm", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd(":verify cancel @user4:example.com MYDEVICE", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd(":verify mismatch a b c d e f", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + } + + #[test] + fn test_cmd_join() { + let mut cmds = setup_commands(); + let ctx = ProgramContext::default(); + + let res = cmds.input_cmd("join #foobar:example.com", ctx.clone()).unwrap(); + let act = WindowAction::Switch(OpenTarget::Name("#foobar:example.com".into())); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd("join #", ctx.clone()).unwrap(); + let act = WindowAction::Switch(OpenTarget::Alternate); + assert_eq!(res, vec![(act.into(), ctx.clone())]); + + let res = cmds.input_cmd("join", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + + let res = cmds.input_cmd("join foo bar", ctx.clone()); + assert_eq!(res, Err(CommandError::InvalidArgument)); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..96001db --- /dev/null +++ b/src/config.rs @@ -0,0 +1,203 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::BufReader; +use std::path::{Path, PathBuf}; +use std::process; + +use clap::Parser; +use matrix_sdk::ruma::OwnedUserId; +use serde::Deserialize; +use url::Url; + +macro_rules! usage { + ( $($args: tt)* ) => { + println!($($args)*); + process::exit(2); + } +} + +fn is_profile_char(c: char) -> bool { + c.is_ascii_alphanumeric() || c == '.' || c == '-' +} + +fn validate_profile_name(name: &str) -> bool { + if name.is_empty() { + return false; + } + + let mut chars = name.chars(); + + if !chars.next().map_or(false, |c| c.is_ascii_alphanumeric()) { + return false; + } + + name.chars().all(is_profile_char) +} + +fn validate_profile_names(names: &HashMap) { + for name in names.keys() { + if validate_profile_name(name.as_str()) { + continue; + } + + usage!( + "{:?} is not a valid profile name.\n\n\ + Profile names can only contain the characters \ + a-z, A-Z, and 0-9. Period (.) and hyphen (-) are allowed after the first character.", + name + ); + } +} + +#[derive(Parser)] +#[clap(version, about, long_about = None)] +#[clap(propagate_version = true)] +pub struct Iamb { + #[clap(short = 'P', long, value_parser)] + pub profile: Option, + + #[clap(short = 'C', long, value_parser)] + pub config_directory: Option, +} + +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + #[error("Error reading configuration file: {0}")] + IO(#[from] std::io::Error), + + #[error("Error loading configuration file: {0}")] + Invalid(#[from] serde_json::Error), +} + +#[derive(Clone, Deserialize)] +pub struct ProfileConfig { + pub user_id: OwnedUserId, + pub url: Url, +} + +#[derive(Clone, Deserialize)] +pub struct IambConfig { + pub profiles: HashMap, + pub default_profile: Option, + pub cache: Option, +} + +impl IambConfig { + pub fn load(config_json: &Path) -> Result { + if !config_json.is_file() { + usage!( + "Please create a configuration file at {}\n\n\ + For more information try '--help'", + config_json.display(), + ); + } + + let file = File::open(config_json)?; + let reader = BufReader::new(file); + let config = serde_json::from_reader(reader)?; + + Ok(config) + } +} + +#[derive(Clone)] +pub struct ApplicationSettings { + pub matrix_dir: PathBuf, + pub cache_dir: PathBuf, + pub session_json: PathBuf, + pub profile_name: String, + pub profile: ProfileConfig, +} + +impl ApplicationSettings { + pub fn load(cli: Iamb) -> Result> { + let mut config_dir = cli.config_directory.or_else(dirs::config_dir).unwrap_or_else(|| { + usage!( + "No user configuration directory found;\ + please specify one via -C.\n\n + For more information try '--help'" + ); + }); + config_dir.push("iamb"); + let mut config_json = config_dir.clone(); + config_json.push("config.json"); + + let IambConfig { mut profiles, default_profile, cache } = + IambConfig::load(config_json.as_path())?; + + validate_profile_names(&profiles); + + let (profile_name, profile) = if let Some(profile) = cli.profile.or(default_profile) { + profiles.remove_entry(&profile).unwrap_or_else(|| { + usage!( + "No configured profile with the name {:?} in {}", + profile, + config_json.display() + ); + }) + } else if profiles.len() == 1 { + profiles.into_iter().next().unwrap() + } else { + usage!( + "No profile specified. \ + Please use -P or add \"default_profile\" to {}.\n\n\ + For more information try '--help'", + config_json.display() + ); + }; + + let mut profile_dir = config_dir.clone(); + profile_dir.push("profiles"); + profile_dir.push(profile_name.as_str()); + + let mut matrix_dir = profile_dir.clone(); + matrix_dir.push("matrix"); + + let mut session_json = profile_dir; + session_json.push("session.json"); + + let cache_dir = cache.unwrap_or_else(|| { + let mut cache = dirs::cache_dir().expect("no user cache directory"); + cache.push("iamb"); + cache + }); + + let settings = ApplicationSettings { + matrix_dir, + cache_dir, + session_json, + profile_name, + profile, + }; + + Ok(settings) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_profile_name_invalid() { + assert_eq!(validate_profile_name(""), false); + assert_eq!(validate_profile_name(" "), false); + assert_eq!(validate_profile_name("a b"), false); + assert_eq!(validate_profile_name("foo^bar"), false); + assert_eq!(validate_profile_name("FOO/BAR"), false); + assert_eq!(validate_profile_name("-b-c"), false); + assert_eq!(validate_profile_name("-B-c"), false); + assert_eq!(validate_profile_name(".b-c"), false); + assert_eq!(validate_profile_name(".B-c"), false); + } + + #[test] + fn test_profile_name_valid() { + assert_eq!(validate_profile_name("foo"), true); + assert_eq!(validate_profile_name("FOO"), true); + assert_eq!(validate_profile_name("a-b-c"), true); + assert_eq!(validate_profile_name("a-B-c"), true); + assert_eq!(validate_profile_name("a.b-c"), true); + assert_eq!(validate_profile_name("a.B-c"), true); + } +} diff --git a/src/keybindings.rs b/src/keybindings.rs new file mode 100644 index 0000000..cf2a7fa --- /dev/null +++ b/src/keybindings.rs @@ -0,0 +1,62 @@ +use modalkit::{ + editing::action::WindowAction, + editing::base::WordStyle, + env::vim::keybindings::{InputStep, VimBindings}, + env::vim::VimMode, + input::bindings::{EdgeEvent, EdgeRepeat, InputBindings}, + input::key::TerminalKey, +}; + +use crate::base::{IambAction, Keybindings}; + +/// Find the boundaries for a Matrix username, room alias, or room ID. +/// +/// Technically "[" and "]" should be here since IPv6 addresses are allowed +/// in the server name, but in practice that should be uncommon, and people +/// can just use `gf` and friends in Visual mode instead. +fn is_mxid_char(c: char) -> bool { + return c >= 'a' && c <= 'z' || + c >= 'A' && c <= 'Z' || + c >= '0' && c <= '9' || + ":-./@_#!".contains(c); +} + +pub fn setup_keybindings() -> Keybindings { + let mut ism = Keybindings::empty(); + + let vim = VimBindings::default() + .submit_on_enter() + .cursor_open(WordStyle::CharSet(is_mxid_char)); + + vim.setup(&mut ism); + + let ctrl_w = EdgeEvent::Key("".parse::().unwrap()); + let ctrl_m = EdgeEvent::Key("".parse::().unwrap()); + let ctrl_z = EdgeEvent::Key("".parse::().unwrap()); + let key_m_lc = EdgeEvent::Key("m".parse::().unwrap()); + let key_z_lc = EdgeEvent::Key("z".parse::().unwrap()); + + let cwz = vec![ + (EdgeRepeat::Once, ctrl_w.clone()), + (EdgeRepeat::Once, key_z_lc), + ]; + let cwcz = vec![ + (EdgeRepeat::Once, ctrl_w.clone()), + (EdgeRepeat::Once, ctrl_z), + ]; + let zoom = InputStep::new().actions(vec![WindowAction::ZoomToggle.into()]); + + ism.add_mapping(VimMode::Normal, &cwz, &zoom); + ism.add_mapping(VimMode::Normal, &cwcz, &zoom); + + let cwm = vec![ + (EdgeRepeat::Once, ctrl_w.clone()), + (EdgeRepeat::Once, key_m_lc), + ]; + let cwcm = vec![(EdgeRepeat::Once, ctrl_w), (EdgeRepeat::Once, ctrl_m)]; + let stoggle = InputStep::new().actions(vec![IambAction::ToggleScrollbackFocus.into()]); + ism.add_mapping(VimMode::Normal, &cwm, &stoggle); + ism.add_mapping(VimMode::Normal, &cwcm, &stoggle); + + return ism; +} diff --git a/src/main.rs b/src/main.rs index ee80563..23e2479 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,493 @@ -fn main() { - unimplemented!(); +#![allow(clippy::manual_range_contains)] +#![allow(clippy::needless_return)] +#![allow(clippy::result_large_err)] +#![allow(clippy::bool_assert_comparison)] +use std::collections::VecDeque; +use std::convert::TryFrom; +use std::fmt::Display; +use std::fs::{create_dir_all, File}; +use std::io::{stdout, BufReader, Stdout}; +use std::ops::DerefMut; +use std::process; +use std::sync::Arc; +use std::time::Duration; + +use clap::Parser; +use tokio::sync::Mutex as AsyncMutex; +use tracing::{self, Level}; +use tracing_subscriber::FmtSubscriber; + +use matrix_sdk::ruma::OwnedUserId; + +use modalkit::crossterm::{ + self, + cursor::Show as CursorShow, + event::{poll, read, Event}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, SetTitle}, +}; + +use modalkit::tui::{ + backend::CrosstermBackend, + layout::Rect, + style::{Color, Style}, + text::Span, + widgets::Paragraph, + Terminal, +}; + +mod base; +mod commands; +mod config; +mod keybindings; +mod message; +mod windows; +mod worker; + +#[cfg(test)] +mod tests; + +use crate::{ + base::{ + AsyncProgramStore, + ChatStore, + IambAction, + IambBufferId, + IambError, + IambId, + IambInfo, + IambResult, + ProgramAction, + ProgramCommands, + ProgramContext, + ProgramStore, + }, + config::{ApplicationSettings, Iamb}, + message::{Message, MessageContent, MessageTimeStamp}, + windows::IambWindow, + worker::{ClientWorker, LoginStyle, Requester}, +}; + +use modalkit::{ + editing::{ + action::{ + Action, + Commandable, + EditError, + EditInfo, + Editable, + Jumpable, + Promptable, + Scrollable, + TabContainer, + TabCount, + WindowAction, + WindowContainer, + }, + base::{OpenTarget, RepeatType}, + context::Resolve, + key::KeyManager, + store::Store, + }, + input::{bindings::BindingMachine, key::TerminalKey}, + widgets::{ + cmdbar::CommandBarState, + screen::{Screen, ScreenState}, + TerminalCursor, + TerminalExtOps, + Window, + }, +}; + +struct Application { + store: AsyncProgramStore, + worker: Requester, + terminal: Terminal>, + bindings: KeyManager, + actstack: VecDeque<(ProgramAction, ProgramContext)>, + cmds: ProgramCommands, + screen: ScreenState, +} + +impl Application { + pub async fn new( + settings: ApplicationSettings, + store: AsyncProgramStore, + ) -> IambResult { + let mut stdout = stdout(); + crossterm::terminal::enable_raw_mode()?; + crossterm::execute!(stdout, EnterAlternateScreen)?; + + let title = format!("iamb ({})", settings.profile.user_id); + crossterm::execute!(stdout, SetTitle(title))?; + + let backend = CrosstermBackend::new(stdout); + let terminal = Terminal::new(backend)?; + + let bindings = crate::keybindings::setup_keybindings(); + let bindings = KeyManager::new(bindings); + let cmds = crate::commands::setup_commands(); + + let mut locked = store.lock().await; + let win = IambWindow::open(IambId::Welcome, locked.deref_mut()).unwrap(); + let cmd = CommandBarState::new(IambBufferId::Command, locked.deref_mut()); + let screen = ScreenState::new(win, cmd); + + let worker = locked.application.worker.clone(); + drop(locked); + + let actstack = VecDeque::new(); + + Ok(Application { + store, + worker, + terminal, + bindings, + actstack, + cmds, + screen, + }) + } + + fn redraw(&mut self, full: bool, store: &mut ProgramStore) -> Result<(), std::io::Error> { + let modestr = self.bindings.showmode(); + let cursor = self.bindings.get_cursor_indicator(); + let sstate = &mut self.screen; + let term = &mut self.terminal; + + if full { + term.clear()?; + } + + term.draw(|f| { + let area = f.size(); + + let screen = Screen::new(store).showmode(modestr); + f.render_stateful_widget(screen, area, sstate); + + if let Some((cx, cy)) = sstate.get_term_cursor() { + if let Some(c) = cursor { + let style = Style::default().fg(Color::Green); + let span = Span::styled(c.to_string(), style); + let para = Paragraph::new(span); + let inner = Rect::new(cx, cy, 1, 1); + f.render_widget(para, inner) + } + f.set_cursor(cx, cy); + } + + store.application.load_older(area.height as u32); + })?; + + Ok(()) + } + + async fn step(&mut self) -> Result { + loop { + self.redraw(false, self.store.clone().lock().await.deref_mut())?; + + if !poll(Duration::from_millis(500))? { + continue; + } + + match read()? { + Event::Key(ke) => return Ok(ke.into()), + Event::Mouse(_) => { + // Do nothing for now. + }, + Event::FocusGained | Event::FocusLost => { + // Do nothing for now. + }, + Event::Resize(_, _) => { + // We'll redraw for the new size next time step() is called. + }, + Event::Paste(_) => { + // Do nothing for now. + }, + } + } + } + + fn action_prepend(&mut self, acts: Vec<(ProgramAction, ProgramContext)>) { + let mut acts = VecDeque::from(acts); + acts.append(&mut self.actstack); + self.actstack = acts; + } + + fn action_pop(&mut self, keyskip: bool) -> Option<(ProgramAction, ProgramContext)> { + if let res @ Some(_) = self.actstack.pop_front() { + return res; + } + + if keyskip { + return None; + } else { + return self.bindings.pop(); + } + } + + fn action_run( + &mut self, + action: ProgramAction, + ctx: ProgramContext, + store: &mut ProgramStore, + ) -> IambResult { + let info = match action { + // Do nothing. + Action::NoOp => None, + + Action::Editor(act) => { + match self.screen.editor_command(&act, &ctx, store) { + Ok(info) => info, + Err(EditError::WrongBuffer(content)) if act.is_switchable(&ctx) => { + // Switch to the right window. + if let Some(winid) = content.to_window() { + let open = OpenTarget::Application(winid); + let open = WindowAction::Switch(open); + let _ = self.screen.window_command(&open, &ctx, store)?; + + // Run command again. + self.screen.editor_command(&act, &ctx, store)? + } else { + return Err(EditError::WrongBuffer(content).into()); + } + }, + Err(err) => return Err(err.into()), + } + }, + + // Simple delegations. + Action::Application(act) => self.iamb_run(act, ctx, store)?, + Action::CommandBar(act) => self.screen.command_bar(&act, &ctx)?, + Action::Macro(act) => self.bindings.macro_command(&act, &ctx, store)?, + Action::Scroll(style) => self.screen.scroll(&style, &ctx, store)?, + Action::Suspend => self.terminal.program_suspend()?, + Action::Tab(cmd) => self.screen.tab_command(&cmd, &ctx, store)?, + Action::Window(cmd) => self.screen.window_command(&cmd, &ctx, store)?, + + Action::Jump(l, dir, count) => { + let count = ctx.resolve(&count); + let _ = self.screen.jump(l, dir, count, &ctx)?; + + None + }, + + // UI actions. + Action::RedrawScreen => { + self.screen.clear_message(); + self.redraw(true, store)?; + + None + }, + + // Actions that create more Actions. + Action::Prompt(act) => { + let acts = self.screen.prompt(&act, &ctx, store)?; + self.action_prepend(acts); + + None + }, + Action::Command(act) => { + let acts = self.cmds.command(&act, &ctx)?; + self.action_prepend(acts); + + None + }, + Action::Repeat(rt) => { + self.bindings.repeat(rt, Some(ctx)); + + None + }, + + // Unimplemented. + Action::KeywordLookup => { + // XXX: implement + None + }, + + _ => { + // XXX: log unhandled actions? print message? + None + }, + }; + + return Ok(info); + } + + fn iamb_run( + &mut self, + action: IambAction, + _: ProgramContext, + store: &mut ProgramStore, + ) -> IambResult { + let info = match action { + IambAction::ToggleScrollbackFocus => { + self.screen.current_window_mut()?.focus_toggle(); + + None + }, + + IambAction::SendMessage(room_id, msg) => { + let (event_id, msg) = self.worker.send_message(room_id.clone(), msg)?; + let user = store.application.settings.profile.user_id.clone(); + let info = store.application.get_room_info(room_id); + let key = (MessageTimeStamp::LocalEcho, event_id); + let msg = MessageContent::Original(msg.into()); + let msg = Message::new(msg, user, MessageTimeStamp::LocalEcho); + info.messages.insert(key, msg); + + None + }, + IambAction::Verify(act, user_dev) => { + if let Some(sas) = store.application.verifications.get(&user_dev) { + self.worker.verify(act, sas.clone())? + } else { + return Err(IambError::InvalidVerificationId(user_dev).into()); + } + }, + IambAction::VerifyRequest(user_id) => { + if let Ok(user_id) = OwnedUserId::try_from(user_id.as_str()) { + self.worker.verify_request(user_id)? + } else { + return Err(IambError::InvalidUserId(user_id).into()); + } + }, + }; + + Ok(info) + } + + pub async fn run(&mut self) -> Result<(), std::io::Error> { + self.terminal.clear()?; + + let store = self.store.clone(); + + while self.screen.tabs() != 0 { + let key = self.step().await?; + + self.bindings.input_key(key); + + let mut locked = store.lock().await; + let mut keyskip = false; + + while let Some((action, ctx)) = self.action_pop(keyskip) { + match self.action_run(action, ctx, locked.deref_mut()) { + Ok(None) => { + // Continue processing. + continue; + }, + Ok(Some(info)) => { + self.screen.push_info(info); + + // Continue processing; we'll redraw later. + continue; + }, + Err(e) => { + self.screen.push_error(e); + + // Skip processing any more keypress Actions until the next key. + keyskip = true; + continue; + }, + } + } + } + + crossterm::terminal::disable_raw_mode()?; + execute!(self.terminal.backend_mut(), LeaveAlternateScreen)?; + self.terminal.show_cursor()?; + + return Ok(()); + } +} + +fn login(worker: Requester, settings: &ApplicationSettings) -> IambResult<()> { + println!("Logging in for {}...", settings.profile.user_id); + + if settings.session_json.is_file() { + let file = File::open(settings.session_json.as_path())?; + let reader = BufReader::new(file); + let session = serde_json::from_reader(reader).map_err(IambError::from)?; + + worker.login(LoginStyle::SessionRestore(session))?; + + return Ok(()); + } + + loop { + let password = rpassword::prompt_password("Password: ")?; + + match worker.login(LoginStyle::Password(password)) { + Ok(info) => { + if let Some(msg) = info { + println!("{}", msg); + } + + break; + }, + Err(err) => { + println!("Failed to login: {}", err); + continue; + }, + } + } + + Ok(()) +} + +fn print_exit(v: T) -> N { + println!("{}", v); + process::exit(2); +} + +#[tokio::main] +async fn main() -> IambResult<()> { + // Parse command-line flags. + let iamb = Iamb::parse(); + + // Load configuration and set up the Matrix SDK. + let settings = ApplicationSettings::load(iamb).unwrap_or_else(print_exit); + + // Set up the tracing subscriber so we can log client messages. + let log_prefix = format!("iamb-log-{}", settings.profile_name); + let mut log_dir = settings.cache_dir.clone(); + log_dir.push("logs"); + + create_dir_all(settings.matrix_dir.as_path())?; + create_dir_all(log_dir.as_path())?; + + let appender = tracing_appender::rolling::daily(log_dir, log_prefix); + let (appender, _) = tracing_appender::non_blocking(appender); + + let subscriber = FmtSubscriber::builder() + .with_writer(appender) + .with_max_level(Level::WARN) + .finish(); + tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); + + // Set up the async worker thread and global store. + let worker = ClientWorker::spawn(settings.clone()); + let store = ChatStore::new(worker.clone(), settings.clone()); + let store = Store::new(store); + let store = Arc::new(AsyncMutex::new(store)); + worker.init(store.clone()); + + login(worker, &settings).unwrap_or_else(print_exit); + + // Make sure panics clean up the terminal properly. + let orig_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info| { + let _ = crossterm::terminal::disable_raw_mode(); + let _ = crossterm::execute!(stdout(), LeaveAlternateScreen); + let _ = crossterm::execute!(stdout(), CursorShow); + orig_hook(panic_info); + process::exit(1); + })); + + let mut application = Application::new(settings, store).await?; + + // We can now run the application. + application.run().await?; + + process::exit(0); } diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 0000000..8005626 --- /dev/null +++ b/src/message.rs @@ -0,0 +1,644 @@ +use std::borrow::Cow; +use std::cmp::{Ord, Ordering, PartialOrd}; +use std::collections::hash_map::DefaultHasher; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::hash::{Hash, Hasher}; +use std::str::Lines; + +use chrono::{DateTime, NaiveDateTime, Utc}; +use unicode_segmentation::UnicodeSegmentation; +use unicode_width::UnicodeWidthStr; + +use matrix_sdk::ruma::{ + events::{ + room::message::{MessageType, RoomMessageEventContent}, + MessageLikeEvent, + }, + MilliSecondsSinceUnixEpoch, + OwnedEventId, + OwnedUserId, + UInt, +}; + +use modalkit::tui::{ + style::{Color, Modifier as StyleModifier, Style}, + text::{Span, Spans, Text}, +}; + +use modalkit::editing::{base::ViewportContext, cursor::Cursor}; + +use crate::base::{IambResult, RoomInfo}; + +pub type MessageEvent = MessageLikeEvent; +pub type MessageFetchResult = IambResult<(Option, Vec)>; +pub type MessageKey = (MessageTimeStamp, OwnedEventId); +pub type Messages = BTreeMap; + +const COLORS: [Color; 13] = [ + Color::Blue, + Color::Cyan, + Color::Green, + Color::LightBlue, + Color::LightGreen, + Color::LightCyan, + Color::LightMagenta, + Color::LightRed, + Color::LightYellow, + Color::Magenta, + Color::Red, + Color::Reset, + Color::Yellow, +]; + +const USER_GUTTER: usize = 30; +const TIME_GUTTER: usize = 12; +const MIN_MSG_LEN: usize = 30; + +const USER_GUTTER_EMPTY: &str = " "; +const USER_GUTTER_EMPTY_SPAN: Span<'static> = Span { + content: Cow::Borrowed(USER_GUTTER_EMPTY), + style: Style { + fg: None, + bg: None, + add_modifier: StyleModifier::empty(), + sub_modifier: StyleModifier::empty(), + }, +}; + +struct WrappedLinesIterator<'a> { + iter: Lines<'a>, + curr: Option<&'a str>, + width: usize, +} + +impl<'a> WrappedLinesIterator<'a> { + fn new(input: &'a str, width: usize) -> Self { + WrappedLinesIterator { iter: input.lines(), curr: None, width } + } +} + +impl<'a> Iterator for WrappedLinesIterator<'a> { + type Item = (&'a str, usize); + + fn next(&mut self) -> Option { + if self.curr.is_none() { + self.curr = self.iter.next(); + } + + if let Some(s) = self.curr.take() { + let width = UnicodeWidthStr::width(s); + + if width <= self.width { + return Some((s, width)); + } else { + // Find where to split the line. + let mut width = 0; + let mut idx = 0; + + for (i, g) in UnicodeSegmentation::grapheme_indices(s, true) { + let gw = UnicodeWidthStr::width(g); + idx = i; + + if width + gw > self.width { + break; + } + + width += gw; + } + + self.curr = Some(&s[idx..]); + + return Some((&s[..idx], width)); + } + } else { + return None; + } + } +} + +fn wrap(input: &str, width: usize) -> WrappedLinesIterator<'_> { + WrappedLinesIterator::new(input, width) +} + +fn space(width: usize) -> String { + " ".repeat(width) +} + +#[derive(thiserror::Error, Debug)] +pub enum TimeStampIntError { + #[error("Integer conversion error: {0}")] + IntError(#[from] std::num::TryFromIntError), + + #[error("UInt conversion error: {0}")] + UIntError(>::Error), +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MessageTimeStamp { + OriginServer(UInt), + LocalEcho, +} + +impl MessageTimeStamp { + fn show(&self) -> Option { + match self { + MessageTimeStamp::OriginServer(ts) => { + let time = i64::from(*ts) / 1000; + let time = NaiveDateTime::from_timestamp_opt(time, 0)?; + let time = DateTime::::from_utc(time, Utc); + let time = time.format("%T"); + let time = format!(" [{}]", time); + + Span::raw(time).into() + }, + MessageTimeStamp::LocalEcho => None, + } + } + + fn is_local_echo(&self) -> bool { + matches!(self, MessageTimeStamp::LocalEcho) + } +} + +impl Ord for MessageTimeStamp { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + (MessageTimeStamp::OriginServer(_), MessageTimeStamp::LocalEcho) => Ordering::Less, + (MessageTimeStamp::OriginServer(a), MessageTimeStamp::OriginServer(b)) => a.cmp(b), + (MessageTimeStamp::LocalEcho, MessageTimeStamp::OriginServer(_)) => Ordering::Greater, + (MessageTimeStamp::LocalEcho, MessageTimeStamp::LocalEcho) => Ordering::Equal, + } + } +} + +impl PartialOrd for MessageTimeStamp { + fn partial_cmp(&self, other: &Self) -> Option { + self.cmp(other).into() + } +} + +impl From for MessageTimeStamp { + fn from(millis: MilliSecondsSinceUnixEpoch) -> Self { + MessageTimeStamp::OriginServer(millis.0) + } +} + +impl TryFrom<&MessageTimeStamp> for usize { + type Error = TimeStampIntError; + + fn try_from(ts: &MessageTimeStamp) -> Result { + let n = match ts { + MessageTimeStamp::LocalEcho => 0, + MessageTimeStamp::OriginServer(u) => usize::try_from(u64::from(*u))?, + }; + + Ok(n) + } +} + +impl TryFrom for MessageTimeStamp { + type Error = TimeStampIntError; + + fn try_from(u: usize) -> Result { + if u == 0 { + Ok(MessageTimeStamp::LocalEcho) + } else { + let n = u64::try_from(u)?; + let n = UInt::try_from(n).map_err(TimeStampIntError::UIntError)?; + + Ok(MessageTimeStamp::OriginServer(n)) + } + } +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct MessageCursor { + /// When timestamp is None, the corner is determined by moving backwards from + /// the most recently received message. + pub timestamp: Option, + + /// A row within the [Text] representation of a [Message]. + pub text_row: usize, +} + +impl MessageCursor { + pub fn new(timestamp: MessageKey, text_row: usize) -> Self { + MessageCursor { timestamp: Some(timestamp), text_row } + } + + /// Get a cursor that refers to the most recent message. + pub fn latest() -> Self { + MessageCursor::default() + } + + pub fn to_key<'a>(&'a self, info: &'a RoomInfo) -> Option<&'a MessageKey> { + if let Some(ref key) = self.timestamp { + Some(key) + } else { + Some(info.messages.last_key_value()?.0) + } + } + + pub fn from_cursor(cursor: &Cursor, info: &RoomInfo) -> Option { + let ev_hash = u64::try_from(cursor.get_x()).ok()?; + let ev_term = OwnedEventId::try_from("$").ok()?; + + let ts_start = MessageTimeStamp::try_from(cursor.get_y()).ok()?; + let start = (ts_start, ev_term); + let mut mc = None; + + for ((ts, event_id), _) in info.messages.range(start..) { + let mut hasher = DefaultHasher::new(); + event_id.hash(&mut hasher); + + if hasher.finish() == ev_hash { + mc = Self::from((*ts, event_id.clone())).into(); + break; + } + + if mc.is_none() { + mc = Self::from((*ts, event_id.clone())).into(); + } + + if ts > &ts_start { + break; + } + } + + return mc; + } + + pub fn to_cursor(&self, info: &RoomInfo) -> Option { + let (ts, event_id) = self.to_key(info)?; + + let y: usize = usize::try_from(ts).ok()?; + + let mut hasher = DefaultHasher::new(); + event_id.hash(&mut hasher); + let x = usize::try_from(hasher.finish()).ok()?; + + Cursor::new(y, x).into() + } +} + +impl From> for MessageCursor { + fn from(key: Option) -> Self { + MessageCursor { timestamp: key, text_row: 0 } + } +} + +impl From for MessageCursor { + fn from(key: MessageKey) -> Self { + MessageCursor { timestamp: Some(key), text_row: 0 } + } +} + +impl Ord for MessageCursor { + fn cmp(&self, other: &Self) -> Ordering { + match (&self.timestamp, &other.timestamp) { + (None, None) => self.text_row.cmp(&other.text_row), + (None, Some(_)) => Ordering::Greater, + (Some(_), None) => Ordering::Less, + (Some(st), Some(ot)) => { + let pcmp = st.cmp(ot); + let tcmp = self.text_row.cmp(&other.text_row); + + pcmp.then(tcmp) + }, + } + } +} + +impl PartialOrd for MessageCursor { + fn partial_cmp(&self, other: &Self) -> Option { + self.cmp(other).into() + } +} + +#[derive(Clone)] +pub enum MessageContent { + Original(Box), + Redacted, +} + +impl AsRef for MessageContent { + fn as_ref(&self) -> &str { + match self { + MessageContent::Original(ev) => { + match &ev.msgtype { + MessageType::Text(content) => { + return content.body.as_ref(); + }, + MessageType::Emote(content) => { + return content.body.as_ref(); + }, + MessageType::Notice(content) => { + return content.body.as_str(); + }, + MessageType::ServerNotice(_) => { + // XXX: implement + + return "[server notice]"; + }, + MessageType::VerificationRequest(_) => { + // XXX: implement + + return "[verification request]"; + }, + MessageType::Audio(..) => { + return "[audio]"; + }, + MessageType::File(..) => { + return "[file]"; + }, + MessageType::Image(..) => { + return "[image]"; + }, + MessageType::Video(..) => { + return "[video]"; + }, + _ => return "[unknown message type]", + } + }, + MessageContent::Redacted => "[redacted]", + } + } +} + +#[derive(Clone)] +pub struct Message { + pub content: MessageContent, + pub sender: OwnedUserId, + pub timestamp: MessageTimeStamp, +} + +impl Message { + pub fn new(content: MessageContent, sender: OwnedUserId, timestamp: MessageTimeStamp) -> Self { + Message { content, sender, timestamp } + } + + pub fn show(&self, selected: bool, vwctx: &ViewportContext) -> Text { + let width = vwctx.get_width(); + let msg = self.as_ref(); + + let mut lines = vec![]; + + let mut style = Style::default(); + + if selected { + style = style.add_modifier(StyleModifier::REVERSED) + } + + if self.timestamp.is_local_echo() { + style = style.add_modifier(StyleModifier::ITALIC); + } + + if USER_GUTTER + TIME_GUTTER + MIN_MSG_LEN <= width { + let lw = width - USER_GUTTER - TIME_GUTTER; + + for (i, (line, w)) in wrap(msg, lw).enumerate() { + let line = Span::styled(line, style); + let trailing = Span::styled(space(lw.saturating_sub(w)), style); + + if i == 0 { + let user = self.show_sender(true); + + if let Some(time) = self.timestamp.show() { + lines.push(Spans(vec![user, line, trailing, time])) + } else { + lines.push(Spans(vec![user, line, trailing])) + } + } else { + let space = USER_GUTTER_EMPTY_SPAN; + + lines.push(Spans(vec![space, line, trailing])) + } + } + } else if USER_GUTTER + MIN_MSG_LEN <= width { + let lw = width - USER_GUTTER; + + for (i, (line, w)) in wrap(msg, lw).enumerate() { + let line = Span::styled(line, style); + let trailing = Span::styled(space(lw.saturating_sub(w)), style); + + let prefix = if i == 0 { + self.show_sender(true) + } else { + USER_GUTTER_EMPTY_SPAN + }; + + lines.push(Spans(vec![prefix, line, trailing])) + } + } else { + lines.push(Spans::from(self.show_sender(false))); + + for (line, _) in wrap(msg, width.saturating_sub(2)) { + let line = format!(" {}", line); + let line = Span::styled(line, style); + + lines.push(Spans(vec![line])) + } + } + + return Text { lines }; + } + + fn show_sender(&self, align_right: bool) -> Span { + let sender = self.sender.to_string(); + + let mut hasher = DefaultHasher::new(); + sender.hash(&mut hasher); + let color = hasher.finish() as usize % COLORS.len(); + let color = COLORS[color]; + + let bold = Style::default().fg(color).add_modifier(StyleModifier::BOLD); + + let sender = if align_right { + format!("{: >width$} ", sender, width = 28) + } else { + format!("{: for Message { + fn from(event: MessageEvent) -> Self { + match event { + MessageLikeEvent::Original(ev) => { + let content = MessageContent::Original(ev.content.into()); + + Message::new(content, ev.sender, ev.origin_server_ts.into()) + }, + MessageLikeEvent::Redacted(ev) => { + Message::new(MessageContent::Redacted, ev.sender, ev.origin_server_ts.into()) + }, + } + } +} + +impl AsRef for Message { + fn as_ref(&self) -> &str { + self.content.as_ref() + } +} + +impl ToString for Message { + fn to_string(&self) -> String { + self.as_ref().to_string() + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_wrapped_lines_ascii() { + let s = "hello world!\nabcdefghijklmnopqrstuvwxyz\ngoodbye"; + + let mut iter = wrap(s, 100); + assert_eq!(iter.next(), Some(("hello world!", 12))); + assert_eq!(iter.next(), Some(("abcdefghijklmnopqrstuvwxyz", 26))); + assert_eq!(iter.next(), Some(("goodbye", 7))); + assert_eq!(iter.next(), None); + + let mut iter = wrap(s, 5); + assert_eq!(iter.next(), Some(("hello", 5))); + assert_eq!(iter.next(), Some((" worl", 5))); + assert_eq!(iter.next(), Some(("d!", 2))); + assert_eq!(iter.next(), Some(("abcde", 5))); + assert_eq!(iter.next(), Some(("fghij", 5))); + assert_eq!(iter.next(), Some(("klmno", 5))); + assert_eq!(iter.next(), Some(("pqrst", 5))); + assert_eq!(iter.next(), Some(("uvwxy", 5))); + assert_eq!(iter.next(), Some(("z", 1))); + assert_eq!(iter.next(), Some(("goodb", 5))); + assert_eq!(iter.next(), Some(("ye", 2))); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_wrapped_lines_unicode() { + let s = "CHICKEN"; + + let mut iter = wrap(s, 14); + assert_eq!(iter.next(), Some((s, 14))); + assert_eq!(iter.next(), None); + + let mut iter = wrap(s, 5); + assert_eq!(iter.next(), Some(("CH", 4))); + assert_eq!(iter.next(), Some(("IC", 4))); + assert_eq!(iter.next(), Some(("KE", 4))); + assert_eq!(iter.next(), Some(("ï¼®", 2))); + assert_eq!(iter.next(), None); + } + + #[test] + fn test_mc_cmp() { + let mc1 = MessageCursor::from(MSG1_KEY.clone()); + let mc2 = MessageCursor::from(MSG2_KEY.clone()); + let mc3 = MessageCursor::from(MSG3_KEY.clone()); + let mc4 = MessageCursor::from(MSG4_KEY.clone()); + let mc5 = MessageCursor::from(MSG5_KEY.clone()); + + // Everything is equal to itself. + assert_eq!(mc1.cmp(&mc1), Ordering::Equal); + assert_eq!(mc2.cmp(&mc2), Ordering::Equal); + assert_eq!(mc3.cmp(&mc3), Ordering::Equal); + assert_eq!(mc4.cmp(&mc4), Ordering::Equal); + assert_eq!(mc5.cmp(&mc5), Ordering::Equal); + + // Local echo is always greater than an origin server timestamp. + assert_eq!(mc1.cmp(&mc2), Ordering::Greater); + assert_eq!(mc1.cmp(&mc3), Ordering::Greater); + assert_eq!(mc1.cmp(&mc4), Ordering::Greater); + assert_eq!(mc1.cmp(&mc5), Ordering::Greater); + + // mc2 is the smallest timestamp. + assert_eq!(mc2.cmp(&mc1), Ordering::Less); + assert_eq!(mc2.cmp(&mc3), Ordering::Less); + assert_eq!(mc2.cmp(&mc4), Ordering::Less); + assert_eq!(mc2.cmp(&mc5), Ordering::Less); + + // mc3 should be less than mc4 because of its event ID. + assert_eq!(mc3.cmp(&mc1), Ordering::Less); + assert_eq!(mc3.cmp(&mc2), Ordering::Greater); + assert_eq!(mc3.cmp(&mc4), Ordering::Less); + assert_eq!(mc3.cmp(&mc5), Ordering::Less); + + // mc4 should be greater than mc3 because of its event ID. + assert_eq!(mc4.cmp(&mc1), Ordering::Less); + assert_eq!(mc4.cmp(&mc2), Ordering::Greater); + assert_eq!(mc4.cmp(&mc3), Ordering::Greater); + assert_eq!(mc4.cmp(&mc5), Ordering::Less); + + // mc5 is the greatest OriginServer timestamp. + assert_eq!(mc5.cmp(&mc1), Ordering::Less); + assert_eq!(mc5.cmp(&mc2), Ordering::Greater); + assert_eq!(mc5.cmp(&mc3), Ordering::Greater); + assert_eq!(mc5.cmp(&mc4), Ordering::Greater); + } + + #[test] + fn test_mc_to_key() { + let info = mock_room(); + let mc1 = MessageCursor::from(MSG1_KEY.clone()); + let mc2 = MessageCursor::from(MSG2_KEY.clone()); + let mc3 = MessageCursor::from(MSG3_KEY.clone()); + let mc4 = MessageCursor::from(MSG4_KEY.clone()); + let mc5 = MessageCursor::from(MSG5_KEY.clone()); + let mc6 = MessageCursor::latest(); + + let k1 = mc1.to_key(&info).unwrap(); + let k2 = mc2.to_key(&info).unwrap(); + let k3 = mc3.to_key(&info).unwrap(); + let k4 = mc4.to_key(&info).unwrap(); + let k5 = mc5.to_key(&info).unwrap(); + let k6 = mc6.to_key(&info).unwrap(); + + // These should all be equal to their MSGN_KEYs. + assert_eq!(k1, &MSG1_KEY.clone()); + assert_eq!(k2, &MSG2_KEY.clone()); + assert_eq!(k3, &MSG3_KEY.clone()); + assert_eq!(k4, &MSG4_KEY.clone()); + assert_eq!(k5, &MSG5_KEY.clone()); + + // MessageCursor::latest() turns into the largest key (our local echo message). + assert_eq!(k6, &MSG1_KEY.clone()); + + // MessageCursor::latest() fails to convert for a room w/o messages. + let info_empty = RoomInfo::default(); + assert_eq!(mc6.to_key(&info_empty), None); + } + + #[test] + fn test_mc_to_from_cursor() { + let info = mock_room(); + let mc1 = MessageCursor::from(MSG1_KEY.clone()); + let mc2 = MessageCursor::from(MSG2_KEY.clone()); + let mc3 = MessageCursor::from(MSG3_KEY.clone()); + let mc4 = MessageCursor::from(MSG4_KEY.clone()); + let mc5 = MessageCursor::from(MSG5_KEY.clone()); + let mc6 = MessageCursor::latest(); + + let identity = |mc: &MessageCursor| { + let c = mc.to_cursor(&info).unwrap(); + + MessageCursor::from_cursor(&c, &info).unwrap() + }; + + // These should all convert to a Cursor and back to the original value. + assert_eq!(identity(&mc1), mc1); + assert_eq!(identity(&mc2), mc2); + assert_eq!(identity(&mc3), mc3); + assert_eq!(identity(&mc4), mc4); + assert_eq!(identity(&mc5), mc5); + + // MessageCursor::latest() should point at the most recent message after conversion. + assert_eq!(identity(&mc6), mc1); + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..ead5867 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,133 @@ +use std::collections::BTreeMap; + +use matrix_sdk::ruma::{ + event_id, + events::room::message::RoomMessageEventContent, + server_name, + user_id, + EventId, + OwnedRoomId, + OwnedUserId, + RoomId, + UInt, +}; + +use std::path::PathBuf; +use std::sync::mpsc::sync_channel; +use url::Url; + +use lazy_static::lazy_static; + +use crate::{ + base::{ChatStore, ProgramStore, RoomFetchStatus, RoomInfo}, + config::{ApplicationSettings, ProfileConfig}, + message::{ + Message, + MessageContent, + MessageKey, + MessageTimeStamp::{LocalEcho, OriginServer}, + Messages, + }, + worker::Requester, +}; + +lazy_static! { + pub static ref TEST_ROOM1_ID: OwnedRoomId = RoomId::new(server_name!("example.com")).to_owned(); + pub static ref TEST_USER1: OwnedUserId = user_id!("@user1:example.com").to_owned(); + pub static ref TEST_USER2: OwnedUserId = user_id!("@user2:example.com").to_owned(); + pub static ref MSG1_KEY: MessageKey = (LocalEcho, EventId::new(server_name!("example.com"))); + pub static ref MSG2_KEY: MessageKey = + (OriginServer(UInt::new(1).unwrap()), EventId::new(server_name!("example.com"))); + pub static ref MSG3_KEY: MessageKey = ( + OriginServer(UInt::new(2).unwrap()), + event_id!("$5jRz3KfVhaUzXtVj7k:example.com").to_owned() + ); + pub static ref MSG4_KEY: MessageKey = ( + OriginServer(UInt::new(2).unwrap()), + event_id!("$JP6qFV7WyXk5ZnexM3:example.com").to_owned() + ); + pub static ref MSG5_KEY: MessageKey = + (OriginServer(UInt::new(8).unwrap()), EventId::new(server_name!("example.com"))); +} + +pub fn mock_message1() -> Message { + let content = RoomMessageEventContent::text_plain("writhe"); + let content = MessageContent::Original(content.into()); + + Message::new(content, TEST_USER1.clone(), MSG1_KEY.0) +} + +pub fn mock_message2() -> Message { + let content = RoomMessageEventContent::text_plain("helium"); + let content = MessageContent::Original(content.into()); + + Message::new(content, TEST_USER2.clone(), MSG2_KEY.0) +} + +pub fn mock_message3() -> Message { + let content = RoomMessageEventContent::text_plain("this\nis\na\nmultiline\nmessage"); + let content = MessageContent::Original(content.into()); + + Message::new(content, TEST_USER2.clone(), MSG3_KEY.0) +} + +pub fn mock_message4() -> Message { + let content = RoomMessageEventContent::text_plain("help"); + let content = MessageContent::Original(content.into()); + + Message::new(content, TEST_USER1.clone(), MSG4_KEY.0) +} + +pub fn mock_message5() -> Message { + let content = RoomMessageEventContent::text_plain("character"); + let content = MessageContent::Original(content.into()); + + Message::new(content, TEST_USER2.clone(), MSG5_KEY.0) +} + +pub fn mock_messages() -> Messages { + let mut messages = BTreeMap::new(); + + messages.insert(MSG1_KEY.clone(), mock_message1()); + messages.insert(MSG2_KEY.clone(), mock_message2()); + messages.insert(MSG3_KEY.clone(), mock_message3()); + messages.insert(MSG4_KEY.clone(), mock_message4()); + messages.insert(MSG5_KEY.clone(), mock_message5()); + + messages +} + +pub fn mock_room() -> RoomInfo { + RoomInfo { + name: Some("Watercooler Discussion".into()), + messages: mock_messages(), + fetch_id: RoomFetchStatus::NotStarted, + fetch_last: None, + } +} + +pub fn mock_settings() -> ApplicationSettings { + ApplicationSettings { + matrix_dir: PathBuf::new(), + cache_dir: PathBuf::new(), + session_json: PathBuf::new(), + profile_name: "test".into(), + profile: ProfileConfig { + user_id: user_id!("@user:example.com").to_owned(), + url: Url::parse("https://example.com").unwrap(), + }, + } +} + +pub fn mock_store() -> ProgramStore { + let (tx, _) = sync_channel(5); + let worker = Requester { tx }; + + let mut store = ChatStore::new(worker, mock_settings()); + let room_id = TEST_ROOM1_ID.clone(); + let info = mock_room(); + + store.rooms.insert(room_id, info); + + ProgramStore::new(store) +} diff --git a/src/windows/mod.rs b/src/windows/mod.rs new file mode 100644 index 0000000..851ff2b --- /dev/null +++ b/src/windows/mod.rs @@ -0,0 +1,696 @@ +use std::cmp::{Ord, Ordering, PartialOrd}; +use std::collections::hash_map::Entry; + +use matrix_sdk::{ + encryption::verification::{format_emojis, SasVerification}, + room::Room as MatrixRoom, + ruma::RoomId, + DisplayName, +}; + +use modalkit::tui::{ + buffer::Buffer, + layout::Rect, + style::{Modifier as StyleModifier, Style}, + text::{Span, Spans, Text}, + widgets::{Block, Borders, Widget}, +}; + +use modalkit::{ + editing::{ + action::{ + EditError, + EditInfo, + EditResult, + Editable, + EditorAction, + Jumpable, + PromptAction, + Promptable, + Scrollable, + UIError, + WindowAction, + }, + base::{ + CloseFlags, + MoveDir1D, + OpenTarget, + PositionList, + ScrollStyle, + ViewportContext, + WordStyle, + }, + }, + widgets::{ + list::{ListCursor, ListItem, ListState}, + TermOffset, + TerminalCursor, + Window, + WindowOps, + }, +}; + +use super::base::{ + ChatStore, + IambBufferId, + IambId, + IambInfo, + IambResult, + ProgramAction, + ProgramContext, + ProgramStore, +}; + +use self::{room::RoomState, welcome::WelcomeState}; + +pub mod room; +pub mod welcome; + +#[inline] +fn selected_style(selected: bool) -> Style { + if selected { + Style::default().add_modifier(StyleModifier::REVERSED) + } else { + Style::default() + } +} + +#[inline] +fn selected_span(s: &str, selected: bool) -> Span { + Span::styled(s, selected_style(selected)) +} + +#[inline] +fn selected_text(s: &str, selected: bool) -> Text { + Text::from(selected_span(s, selected)) +} + +#[inline] +fn room_prompt( + room_id: &RoomId, + act: &PromptAction, + ctx: &ProgramContext, +) -> EditResult, IambInfo> { + match act { + PromptAction::Submit => { + let room = IambId::Room(room_id.to_owned()); + let open = WindowAction::Switch(OpenTarget::Application(room)); + let acts = vec![(open.into(), ctx.clone())]; + + Ok(acts) + }, + PromptAction::Abort(_) => { + let msg = "Cannot abort entry inside a list"; + let err = EditError::Failure(msg.into()); + + Err(err) + }, + PromptAction::Recall(_, _) => { + let msg = "Cannot recall history inside a list"; + let err = EditError::Failure(msg.into()); + + Err(err) + }, + _ => Err(EditError::Unimplemented("unknown prompt action".to_string())), + } +} + +macro_rules! delegate { + ($s: expr, $id: ident => $e: expr) => { + match $s { + IambWindow::Room($id) => $e, + IambWindow::DirectList($id) => $e, + IambWindow::RoomList($id) => $e, + IambWindow::SpaceList($id) => $e, + IambWindow::VerifyList($id) => $e, + IambWindow::Welcome($id) => $e, + } + }; +} + +pub enum IambWindow { + DirectList(DirectListState), + Room(RoomState), + VerifyList(VerifyListState), + RoomList(RoomListState), + SpaceList(SpaceListState), + Welcome(WelcomeState), +} + +impl IambWindow { + pub fn focus_toggle(&mut self) { + if let IambWindow::Room(w) = self { + w.focus_toggle() + } else { + return; + } + } + + pub fn get_title(&self, store: &mut ProgramStore) -> String { + match self { + IambWindow::Room(w) => w.get_title(store), + IambWindow::DirectList(_) => "Direct Messages".to_string(), + IambWindow::RoomList(_) => "Rooms".to_string(), + IambWindow::SpaceList(_) => "Spaces".to_string(), + IambWindow::VerifyList(_) => "Verifications".to_string(), + IambWindow::Welcome(_) => "Welcome to iamb".to_string(), + } + } +} + +pub type DirectListState = ListState; +pub type RoomListState = ListState; +pub type SpaceListState = ListState; +pub type VerifyListState = ListState; + +impl From for IambWindow { + fn from(room: RoomState) -> Self { + IambWindow::Room(room) + } +} + +impl From for IambWindow { + fn from(list: VerifyListState) -> Self { + IambWindow::VerifyList(list) + } +} + +impl From for IambWindow { + fn from(list: DirectListState) -> Self { + IambWindow::DirectList(list) + } +} + +impl From for IambWindow { + fn from(list: RoomListState) -> Self { + IambWindow::RoomList(list) + } +} + +impl From for IambWindow { + fn from(list: SpaceListState) -> Self { + IambWindow::SpaceList(list) + } +} + +impl From for IambWindow { + fn from(win: WelcomeState) -> Self { + IambWindow::Welcome(win) + } +} + +impl Editable for IambWindow { + fn editor_command( + &mut self, + act: &EditorAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + delegate!(self, w => w.editor_command(act, ctx, store)) + } +} + +impl Jumpable for IambWindow { + fn jump( + &mut self, + list: PositionList, + dir: MoveDir1D, + count: usize, + ctx: &ProgramContext, + ) -> IambResult { + delegate!(self, w => w.jump(list, dir, count, ctx)) + } +} + +impl Scrollable for IambWindow { + fn scroll( + &mut self, + style: &ScrollStyle, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + delegate!(self, w => w.scroll(style, ctx, store)) + } +} + +impl Promptable for IambWindow { + fn prompt( + &mut self, + act: &PromptAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult, IambInfo> { + delegate!(self, w => w.prompt(act, ctx, store)) + } +} + +impl TerminalCursor for IambWindow { + fn get_term_cursor(&self) -> Option { + delegate!(self, w => w.get_term_cursor()) + } +} + +impl WindowOps for IambWindow { + fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { + let title = self.get_title(store); + let block = Block::default().title(title.as_str()).borders(Borders::ALL); + let inner = block.inner(area); + block.render(area, buf); + + match self { + IambWindow::Room(state) => state.draw(inner, buf, focused, store), + IambWindow::DirectList(state) => { + let dms = store.application.worker.direct_messages(); + let items = dms.into_iter().map(|(id, name)| DirectItem::new(id, name, store)); + state.set(items.collect()); + state.draw(inner, buf, focused, store); + }, + IambWindow::RoomList(state) => { + let joined = store.application.worker.joined_rooms(); + let items = joined.into_iter().map(|(id, name)| RoomItem::new(id, name, store)); + state.set(items.collect()); + state.draw(inner, buf, focused, store); + }, + IambWindow::SpaceList(state) => { + let spaces = store.application.worker.spaces(); + let items = + spaces.into_iter().map(|(room, name)| SpaceItem::new(room, name, store)); + state.set(items.collect()); + state.draw(inner, buf, focused, store); + }, + IambWindow::VerifyList(state) => { + let verifications = &store.application.verifications; + let mut items = verifications.iter().map(VerifyItem::from).collect::>(); + + // Sort the active verifications towards the top. + items.sort(); + + state.set(items); + state.draw(inner, buf, focused, store); + }, + IambWindow::Welcome(state) => state.draw(inner, buf, focused, store), + } + } + + fn dup(&self, store: &mut ProgramStore) -> Self { + delegate!(self, w => w.dup(store).into()) + } + + fn close(&mut self, flags: CloseFlags, store: &mut ProgramStore) -> bool { + delegate!(self, w => w.close(flags, store)) + } + + fn get_cursor_word(&self, style: &WordStyle) -> Option { + delegate!(self, w => w.get_cursor_word(style)) + } + + fn get_selected_word(&self) -> Option { + delegate!(self, w => w.get_selected_word()) + } +} + +impl Window for IambWindow { + fn id(&self) -> IambId { + match self { + IambWindow::Room(room) => IambId::Room(room.id().to_owned()), + IambWindow::DirectList(_) => IambId::DirectList, + IambWindow::RoomList(_) => IambId::RoomList, + IambWindow::SpaceList(_) => IambId::SpaceList, + IambWindow::VerifyList(_) => IambId::VerifyList, + IambWindow::Welcome(_) => IambId::Welcome, + } + } + + fn open(id: IambId, store: &mut ProgramStore) -> IambResult { + match id { + IambId::Room(room_id) => { + let (room, name) = store.application.worker.get_room(room_id)?; + let room = RoomState::new(room, name, store); + + return Ok(room.into()); + }, + IambId::DirectList => { + let list = DirectListState::new(IambBufferId::DirectList, vec![]); + + return Ok(list.into()); + }, + IambId::RoomList => { + let list = RoomListState::new(IambBufferId::RoomList, vec![]); + + return Ok(list.into()); + }, + IambId::SpaceList => { + let list = SpaceListState::new(IambBufferId::SpaceList, vec![]); + + return Ok(list.into()); + }, + IambId::VerifyList => { + let list = VerifyListState::new(IambBufferId::VerifyList, vec![]); + + return Ok(list.into()); + }, + IambId::Welcome => { + let win = WelcomeState::new(store); + + return Ok(win.into()); + }, + } + } + + fn find(name: String, store: &mut ProgramStore) -> IambResult { + let ChatStore { names, worker, .. } = &mut store.application; + + match names.entry(name) { + Entry::Vacant(v) => { + let room_id = worker.join_room(v.key().to_string())?; + v.insert(room_id.clone()); + + let (room, name) = store.application.worker.get_room(room_id)?; + let room = RoomState::new(room, name, store); + + Ok(room.into()) + }, + Entry::Occupied(o) => { + let id = IambId::Room(o.get().clone()); + + IambWindow::open(id, store) + }, + } + } + + fn posn(index: usize, _: &mut ProgramStore) -> IambResult { + let msg = format!("Cannot find indexed buffer (index = {})", index); + let err = UIError::Unimplemented(msg); + + Err(err) + } +} + +#[derive(Clone)] +pub struct RoomItem { + room: MatrixRoom, + name: String, +} + +impl RoomItem { + fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self { + let name = name.to_string(); + + store.application.set_room_name(room.room_id(), name.as_str()); + + RoomItem { room, name } + } +} + +impl ToString for RoomItem { + fn to_string(&self) -> String { + return self.name.clone(); + } +} + +impl ListItem for RoomItem { + fn show(&self, selected: bool, _: &ViewportContext, _: &mut ProgramStore) -> Text { + selected_text(self.name.as_str(), selected) + } +} + +impl Promptable for RoomItem { + fn prompt( + &mut self, + act: &PromptAction, + ctx: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + room_prompt(self.room.room_id(), act, ctx) + } +} + +#[derive(Clone)] +pub struct DirectItem { + room: MatrixRoom, + name: String, +} + +impl DirectItem { + fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self { + let name = name.to_string(); + + store.application.set_room_name(room.room_id(), name.as_str()); + + DirectItem { room, name } + } +} + +impl ToString for DirectItem { + fn to_string(&self) -> String { + return self.name.clone(); + } +} + +impl ListItem for DirectItem { + fn show(&self, selected: bool, _: &ViewportContext, _: &mut ProgramStore) -> Text { + selected_text(self.name.as_str(), selected) + } +} + +impl Promptable for DirectItem { + fn prompt( + &mut self, + act: &PromptAction, + ctx: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + room_prompt(self.room.room_id(), act, ctx) + } +} + +#[derive(Clone)] +pub struct SpaceItem { + room: MatrixRoom, + name: String, +} + +impl SpaceItem { + fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self { + let name = name.to_string(); + + store.application.set_room_name(room.room_id(), name.as_str()); + + SpaceItem { room, name } + } +} + +impl ToString for SpaceItem { + fn to_string(&self) -> String { + return self.room.room_id().to_string(); + } +} + +impl ListItem for SpaceItem { + fn show(&self, selected: bool, _: &ViewportContext, _: &mut ProgramStore) -> Text { + selected_text(self.name.as_str(), selected) + } +} + +impl Promptable for SpaceItem { + fn prompt( + &mut self, + act: &PromptAction, + ctx: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + room_prompt(self.room.room_id(), act, ctx) + } +} + +#[derive(Clone)] +pub struct VerifyItem { + user_dev: String, + sasv1: SasVerification, +} + +impl VerifyItem { + fn new(user_dev: String, sasv1: SasVerification) -> Self { + VerifyItem { user_dev, sasv1 } + } + + fn show_item(&self) -> String { + let state = if self.sasv1.is_done() { + "done" + } else if self.sasv1.is_cancelled() { + "cancelled" + } else if self.sasv1.emoji().is_some() { + "accepted" + } else { + "not accepted" + }; + + if self.sasv1.is_self_verification() { + let device = self.sasv1.other_device(); + + if let Some(display_name) = device.display_name() { + format!("Device verification with {} ({})", display_name, state) + } else { + format!("Device verification with device {} ({})", device.device_id(), state) + } + } else { + format!("User Verification with {} ({})", self.sasv1.other_user_id(), state) + } + } +} + +impl PartialEq for VerifyItem { + fn eq(&self, other: &Self) -> bool { + self.user_dev == other.user_dev + } +} + +impl Eq for VerifyItem {} + +impl Ord for VerifyItem { + fn cmp(&self, other: &Self) -> Ordering { + fn state_val(sas: &SasVerification) -> usize { + if sas.is_done() { + return 3; + } else if sas.is_cancelled() { + return 2; + } else { + return 1; + } + } + + fn device_val(sas: &SasVerification) -> usize { + if sas.is_self_verification() { + return 1; + } else { + return 2; + } + } + + let state1 = state_val(&self.sasv1); + let state2 = state_val(&other.sasv1); + + let dev1 = device_val(&self.sasv1); + let dev2 = device_val(&other.sasv1); + + let scmp = state1.cmp(&state2); + let dcmp = dev1.cmp(&dev2); + + scmp.then(dcmp).then_with(|| { + let did1 = self.sasv1.other_device().device_id(); + let did2 = other.sasv1.other_device().device_id(); + + did1.cmp(did2) + }) + } +} + +impl PartialOrd for VerifyItem { + fn partial_cmp(&self, other: &Self) -> Option { + self.cmp(other).into() + } +} + +impl From<(&String, &SasVerification)> for VerifyItem { + fn from((user_dev, sasv1): (&String, &SasVerification)) -> Self { + VerifyItem::new(user_dev.clone(), sasv1.clone()) + } +} + +impl ToString for VerifyItem { + fn to_string(&self) -> String { + if self.sasv1.is_done() { + String::new() + } else if self.sasv1.is_cancelled() { + format!(":verify request {}", self.sasv1.other_user_id()) + } else if self.sasv1.emoji().is_some() { + format!(":verify confirm {}", self.user_dev) + } else { + format!(":verify accept {}", self.user_dev) + } + } +} + +impl ListItem for VerifyItem { + fn show(&self, selected: bool, _: &ViewportContext, _: &mut ProgramStore) -> Text { + let mut lines = vec![]; + + let bold = Style::default().add_modifier(StyleModifier::BOLD); + let item = Span::styled(self.show_item(), selected_style(selected)); + lines.push(Spans::from(item)); + + if self.sasv1.is_done() { + // Print nothing. + } else if self.sasv1.is_cancelled() { + if let Some(info) = self.sasv1.cancel_info() { + lines.push(Spans::from(format!(" Cancelled: {}", info.reason()))); + lines.push(Spans::from("")); + } + + lines.push(Spans::from(" You can start a new verification request with:")); + } else if let Some(emoji) = self.sasv1.emoji() { + lines.push(Spans::from( + " Both devices should see the following Emoji sequence:".to_string(), + )); + lines.push(Spans::from("")); + + for line in format_emojis(emoji).lines() { + lines.push(Spans::from(format!(" {}", line))); + } + + lines.push(Spans::from("")); + lines.push(Spans::from(" If they don't match, run:")); + lines.push(Spans::from("")); + lines.push(Spans::from(Span::styled( + format!(":verify mismatch {}", self.user_dev), + bold, + ))); + lines.push(Spans::from("")); + lines.push(Spans::from(" If everything looks right, you can confirm with:")); + } else { + lines.push(Spans::from(" To accept this request, run:")); + } + + let cmd = self.to_string(); + + if !cmd.is_empty() { + lines.push(Spans::from("")); + lines.push(Spans(vec![Span::from(" "), Span::styled(cmd, bold)])); + lines.push(Spans::from("")); + lines.push(Spans(vec![ + Span::from("You can copy the above command with "), + Span::styled("yy", bold), + Span::from(" and then execute it with "), + Span::styled("@\"", bold), + ])); + } + + Text { lines } + } +} + +impl Promptable for VerifyItem { + fn prompt( + &mut self, + act: &PromptAction, + _: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + match act { + PromptAction::Submit => Ok(vec![]), + PromptAction::Abort(_) => { + let msg = "Cannot abort entry inside a list"; + let err = EditError::Failure(msg.into()); + + Err(err) + }, + PromptAction::Recall(_, _) => { + let msg = "Cannot recall history inside a list"; + let err = EditError::Failure(msg.into()); + + Err(err) + }, + _ => Err(EditError::Unimplemented("unknown prompt action".to_string())), + } + } +} diff --git a/src/windows/room/chat.rs b/src/windows/room/chat.rs new file mode 100644 index 0000000..8d52804 --- /dev/null +++ b/src/windows/room/chat.rs @@ -0,0 +1,319 @@ +use matrix_sdk::{ + room::Room as MatrixRoom, + ruma::{OwnedRoomId, RoomId}, +}; + +use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget}; + +use modalkit::{ + widgets::textbox::{TextBox, TextBoxState}, + widgets::TerminalCursor, + widgets::{PromptActions, WindowOps}, +}; + +use modalkit::editing::{ + action::{ + EditError, + EditInfo, + EditResult, + Editable, + EditorAction, + Jumpable, + PromptAction, + Promptable, + Scrollable, + }, + base::{CloseFlags, Count, MoveDir1D, PositionList, ScrollStyle, WordStyle}, + context::Resolve, + history::{self, HistoryList}, + rope::EditRope, +}; + +use crate::base::{ + IambAction, + IambBufferId, + IambInfo, + IambResult, + ProgramAction, + ProgramContext, + ProgramStore, + RoomFocus, +}; + +use super::scrollback::{Scrollback, ScrollbackState}; + +pub struct ChatState { + room_id: OwnedRoomId, + + tbox: TextBoxState, + sent: HistoryList, + sent_scrollback: history::ScrollbackState, + + scrollback: ScrollbackState, + focus: RoomFocus, +} + +impl ChatState { + pub fn new(room: MatrixRoom, store: &mut ProgramStore) -> Self { + let room_id = room.room_id().to_owned(); + let scrollback = ScrollbackState::new(room_id.clone()); + let id = IambBufferId::Room(room_id.clone(), RoomFocus::MessageBar); + let ebuf = store.load_buffer(id); + let tbox = TextBoxState::new(ebuf); + + ChatState { + room_id, + + tbox, + sent: HistoryList::new(EditRope::from(""), 100), + sent_scrollback: history::ScrollbackState::Pending, + + scrollback, + focus: RoomFocus::MessageBar, + } + } + + pub fn focus_toggle(&mut self) { + self.focus = match self.focus { + RoomFocus::Scrollback => RoomFocus::MessageBar, + RoomFocus::MessageBar => RoomFocus::Scrollback, + }; + } + + pub fn id(&self) -> &RoomId { + &self.room_id + } +} + +macro_rules! delegate { + ($s: expr, $id: ident => $e: expr) => { + match $s.focus { + RoomFocus::Scrollback => { + match $s { + ChatState { scrollback: $id, .. } => $e, + } + }, + RoomFocus::MessageBar => { + match $s { + ChatState { tbox: $id, .. } => $e, + } + }, + } + }; +} + +impl WindowOps for ChatState { + fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { + Chat::new(store).focus(focused).render(area, buf, self) + } + + fn dup(&self, store: &mut ProgramStore) -> Self { + // XXX: I want each WindowSlot to have its own shared buffer, instead of each Room; need to + // find a good way to pass that info here so that it can be part of the content id. + let id = IambBufferId::Room(self.room_id.clone(), RoomFocus::MessageBar); + let ebuf = store.load_buffer(id); + let tbox = TextBoxState::new(ebuf); + + ChatState { + room_id: self.room_id.clone(), + + tbox, + sent: self.sent.clone(), + sent_scrollback: history::ScrollbackState::Pending, + + scrollback: self.scrollback.dup(store), + focus: self.focus, + } + } + + fn close(&mut self, _: CloseFlags, _: &mut ProgramStore) -> bool { + // XXX: what's the right closing behaviour for a room? + // Should write send a message? + true + } + + fn get_cursor_word(&self, style: &WordStyle) -> Option { + delegate!(self, w => w.get_cursor_word(style)) + } + + fn get_selected_word(&self) -> Option { + delegate!(self, w => w.get_selected_word()) + } +} + +impl Editable for ChatState { + fn editor_command( + &mut self, + act: &EditorAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + match delegate!(self, w => w.editor_command(act, ctx, store)) { + res @ Ok(_) => res, + Err(EditError::WrongBuffer(IambBufferId::Room(room_id, focus))) + if room_id == self.room_id && act.is_switchable(ctx) => + { + // Switch focus. + self.focus = focus; + + // Run command again. + delegate!(self, w => w.editor_command(act, ctx, store)) + }, + res @ Err(_) => res, + } + } +} + +impl TerminalCursor for ChatState { + fn get_term_cursor(&self) -> Option<(u16, u16)> { + delegate!(self, w => w.get_term_cursor()) + } +} + +impl Jumpable for ChatState { + fn jump( + &mut self, + list: PositionList, + dir: MoveDir1D, + count: usize, + ctx: &ProgramContext, + ) -> IambResult { + delegate!(self, w => w.jump(list, dir, count, ctx)) + } +} + +impl Scrollable for ChatState { + fn scroll( + &mut self, + style: &ScrollStyle, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + // Send all scroll commands to the scrollback. + // + // If there's enough message text for scrolling to be necessary, + // navigating with movement keys should be enough to do the job. + self.scrollback.scroll(style, ctx, store) + } +} + +impl PromptActions for ChatState { + fn submit( + &mut self, + ctx: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + let txt = self.tbox.reset_text(); + + let act = if txt.is_empty() { + vec![] + } else { + let act = IambAction::SendMessage(self.room_id.clone(), txt).into(); + + vec![(act, ctx.clone())] + }; + + Ok(act) + } + + fn abort( + &mut self, + empty: bool, + _: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + let text = self.tbox.get(); + + if empty && text.is_blank() { + return Ok(vec![]); + } + + let text = self.tbox.reset().trim(); + + if text.is_empty() { + let _ = self.sent.end(); + } else { + self.sent.select(text); + } + + return Ok(vec![]); + } + + fn recall( + &mut self, + dir: &MoveDir1D, + count: &Count, + ctx: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult, IambInfo> { + let count = ctx.resolve(count); + let rope = self.tbox.get(); + + let text = self.sent.recall(&rope, &mut self.sent_scrollback, *dir, count); + + if let Some(text) = text { + self.tbox.set_text(text); + } + + Ok(vec![]) + } +} + +impl Promptable for ChatState { + fn prompt( + &mut self, + act: &PromptAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult, IambInfo> { + if let RoomFocus::Scrollback = self.focus { + return Ok(vec![]); + } + + match act { + PromptAction::Submit => self.submit(ctx, store), + PromptAction::Abort(empty) => self.abort(*empty, ctx, store), + PromptAction::Recall(dir, count) => self.recall(dir, count, ctx, store), + _ => Err(EditError::Unimplemented("unknown prompt action".to_string())), + } + } +} + +pub struct Chat<'a> { + store: &'a mut ProgramStore, + focused: bool, +} + +impl<'a> Chat<'a> { + pub fn new(store: &'a mut ProgramStore) -> Chat<'a> { + Chat { store, focused: false } + } + + pub fn focus(mut self, focused: bool) -> Self { + self.focused = focused; + self + } +} + +impl<'a> StatefulWidget for Chat<'a> { + type State = ChatState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let lines = state.tbox.has_lines(5).max(1) as u16; + let drawh = area.height; + let texth = lines.min(drawh).clamp(1, 5); + let scrollh = drawh.saturating_sub(texth); + + let scrollarea = Rect::new(area.x, area.y, area.width, scrollh); + let textarea = Rect::new(scrollarea.x, scrollarea.y + scrollh, scrollarea.width, texth); + + let scrollback_focused = state.focus.is_scrollback() && self.focused; + let scrollback = Scrollback::new(self.store).focus(scrollback_focused); + scrollback.render(scrollarea, buf, &mut state.scrollback); + + let prompt = if self.focused { "> " } else { " " }; + + let tbox = TextBox::new().prompt(prompt); + tbox.render(textarea, buf, &mut state.tbox); + } +} diff --git a/src/windows/room/mod.rs b/src/windows/room/mod.rs new file mode 100644 index 0000000..0f5722a --- /dev/null +++ b/src/windows/room/mod.rs @@ -0,0 +1,182 @@ +use matrix_sdk::room::Room as MatrixRoom; +use matrix_sdk::ruma::RoomId; +use matrix_sdk::DisplayName; + +use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget}; + +use modalkit::{ + editing::action::{ + EditInfo, + EditResult, + Editable, + EditorAction, + Jumpable, + PromptAction, + Promptable, + Scrollable, + }, + editing::base::{CloseFlags, MoveDir1D, PositionList, ScrollStyle, WordStyle}, + widgets::{TermOffset, TerminalCursor, WindowOps}, +}; + +use crate::base::{IambInfo, IambResult, ProgramAction, ProgramContext, ProgramStore}; + +use self::chat::ChatState; +use self::space::{Space, SpaceState}; + +mod chat; +mod scrollback; +mod space; + +macro_rules! delegate { + ($s: expr, $id: ident => $e: expr) => { + match $s { + RoomState::Chat($id) => $e, + RoomState::Space($id) => $e, + } + }; +} + +pub enum RoomState { + Chat(ChatState), + Space(SpaceState), +} + +impl From for RoomState { + fn from(chat: ChatState) -> Self { + RoomState::Chat(chat) + } +} + +impl From for RoomState { + fn from(space: SpaceState) -> Self { + RoomState::Space(space) + } +} + +impl RoomState { + pub fn new(room: MatrixRoom, name: DisplayName, store: &mut ProgramStore) -> Self { + let room_id = room.room_id().to_owned(); + let info = store.application.get_room_info(room_id); + info.name = name.to_string().into(); + + if room.is_space() { + SpaceState::new(room).into() + } else { + ChatState::new(room, store).into() + } + } + + pub fn get_title(&self, store: &mut ProgramStore) -> String { + store + .application + .rooms + .get(self.id()) + .and_then(|i| i.name.as_ref()) + .map(String::from) + .unwrap_or_else(|| "Untitled Matrix Room".to_string()) + } + + pub fn focus_toggle(&mut self) { + match self { + RoomState::Chat(chat) => chat.focus_toggle(), + RoomState::Space(_) => return, + } + } + + pub fn id(&self) -> &RoomId { + match self { + RoomState::Chat(chat) => chat.id(), + RoomState::Space(space) => space.id(), + } + } +} + +impl Editable for RoomState { + fn editor_command( + &mut self, + act: &EditorAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + delegate!(self, w => w.editor_command(act, ctx, store)) + } +} + +impl Jumpable for RoomState { + fn jump( + &mut self, + list: PositionList, + dir: MoveDir1D, + count: usize, + ctx: &ProgramContext, + ) -> IambResult { + delegate!(self, w => w.jump(list, dir, count, ctx)) + } +} + +impl Scrollable for RoomState { + fn scroll( + &mut self, + style: &ScrollStyle, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + delegate!(self, w => w.scroll(style, ctx, store)) + } +} + +impl Promptable for RoomState { + fn prompt( + &mut self, + act: &PromptAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult, IambInfo> { + delegate!(self, w => w.prompt(act, ctx, store)) + } +} + +impl TerminalCursor for RoomState { + fn get_term_cursor(&self) -> Option { + delegate!(self, w => w.get_term_cursor()) + } +} + +impl WindowOps for RoomState { + fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { + match self { + RoomState::Chat(chat) => chat.draw(area, buf, focused, store), + RoomState::Space(space) => { + Space::new(store).focus(focused).render(area, buf, space); + }, + } + } + + fn dup(&self, store: &mut ProgramStore) -> Self { + match self { + RoomState::Chat(chat) => RoomState::Chat(chat.dup(store)), + RoomState::Space(space) => RoomState::Space(space.dup(store)), + } + } + + fn close(&mut self, _: CloseFlags, _: &mut ProgramStore) -> bool { + // XXX: what's the right closing behaviour for a room? + // Should write send a message? + true + } + + fn get_cursor_word(&self, style: &WordStyle) -> Option { + match self { + RoomState::Chat(chat) => chat.get_cursor_word(style), + RoomState::Space(space) => space.get_cursor_word(style), + } + } + + fn get_selected_word(&self) -> Option { + match self { + RoomState::Chat(chat) => chat.get_selected_word(), + RoomState::Space(space) => space.get_selected_word(), + } + } +} diff --git a/src/windows/room/scrollback.rs b/src/windows/room/scrollback.rs new file mode 100644 index 0000000..efa0db7 --- /dev/null +++ b/src/windows/room/scrollback.rs @@ -0,0 +1,1471 @@ +use std::collections::HashSet; + +use regex::Regex; + +use matrix_sdk::ruma::OwnedRoomId; + +use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget}; +use modalkit::widgets::{ScrollActions, TerminalCursor, WindowOps}; + +use modalkit::editing::{ + action::{ + Action, + CursorAction, + EditAction, + EditError, + EditInfo, + EditResult, + Editable, + EditorAction, + EditorActions, + HistoryAction, + InsertTextAction, + Jumpable, + PromptAction, + Promptable, + Scrollable, + Searchable, + SelectionAction, + UIError, + UIResult, + }, + base::{ + Axis, + CloseFlags, + Count, + EditRange, + EditTarget, + Mark, + MoveDir1D, + MoveDir2D, + MoveDirMod, + MovePosition, + MoveTerminus, + MoveType, + PositionList, + RangeType, + Register, + ScrollSize, + ScrollStyle, + SearchType, + TargetShape, + ViewportContext, + WordStyle, + }, + context::{EditContext, Resolve}, + cursor::{CursorGroup, CursorState}, + history::HistoryList, + rope::EditRope, + store::{RegisterCell, RegisterPutFlags}, +}; + +use crate::{ + base::{IambBufferId, IambInfo, ProgramContext, ProgramStore, RoomFocus, RoomInfo}, + message::{Message, MessageCursor, MessageKey}, +}; + +fn nth_key_before(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageKey { + let mut end = &pos; + let iter = info.messages.range(..=&pos).rev().enumerate(); + + for (i, (key, _)) in iter { + end = key; + + if i >= n { + break; + } + } + + end.clone() +} + +fn nth_before(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageCursor { + nth_key_before(pos, n, info).into() +} + +fn nth_key_after(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageKey { + let mut end = &pos; + let iter = info.messages.range(&pos..).enumerate(); + + for (i, (key, _)) in iter { + end = key; + + if i >= n { + break; + } + } + + end.clone() +} + +fn nth_after(pos: MessageKey, n: usize, info: &RoomInfo) -> MessageCursor { + nth_key_after(pos, n, info).into() +} + +pub struct ScrollbackState { + room_id: OwnedRoomId, + id: IambBufferId, + cursor: MessageCursor, + viewctx: ViewportContext, + jumped: HistoryList, +} + +impl ScrollbackState { + pub fn new(room_id: OwnedRoomId) -> ScrollbackState { + let id = IambBufferId::Room(room_id.to_owned(), RoomFocus::Scrollback); + let cursor = MessageCursor::default(); + let viewctx = ViewportContext::default(); + let jumped = HistoryList::default(); + + ScrollbackState { room_id, id, cursor, viewctx, jumped } + } + + /// Set the dimensions and placement within the terminal window for this list. + pub fn set_term_info(&mut self, area: Rect) { + self.viewctx.dimensions = (area.width as usize, area.height as usize); + } + + pub fn messages<'a>( + &self, + range: EditRange, + info: &'a RoomInfo, + ) -> impl Iterator { + let start = range.start.to_key(info); + let end = range.end.to_key(info); + + let (start, end) = if let (Some(start), Some(end)) = (start, end) { + (start, end) + } else if let Some((last, _)) = info.messages.last_key_value() { + (last, last) + } else { + return info.messages.range(..); + }; + + if range.inclusive { + info.messages.range(start..=end) + } else { + info.messages.range(start..end) + } + } + + fn scrollview(&mut self, idx: MessageKey, pos: MovePosition, info: &RoomInfo) { + let selidx = if let Some(key) = self.cursor.to_key(info) { + key + } else { + return; + }; + + match pos { + MovePosition::Beginning => { + self.viewctx.corner = idx.into(); + }, + MovePosition::Middle => { + let mut lines = 0; + let target = self.viewctx.get_height() / 2; + + for (key, item) in info.messages.range(..=&idx).rev() { + let sel = selidx == key; + let len = item.show(sel, &self.viewctx).lines.len(); + + if key == &idx { + lines += len / 2; + } else { + lines += len; + } + + if lines >= target { + // We've moved back far enough. + self.viewctx.corner.timestamp = key.clone().into(); + self.viewctx.corner.text_row = lines - target; + break; + } + } + }, + MovePosition::End => { + let mut lines = 0; + let target = self.viewctx.get_height(); + + for (key, item) in info.messages.range(..=&idx).rev() { + let sel = key == selidx; + let len = item.show(sel, &self.viewctx).lines.len(); + + lines += len; + + if lines >= target { + // We've moved back far enough. + self.viewctx.corner.timestamp = key.clone().into(); + self.viewctx.corner.text_row = lines - target; + break; + } + } + }, + } + } + + fn shift_cursor(&mut self, info: &RoomInfo) { + let last_key = if let Some(k) = info.messages.last_key_value() { + k.0 + } else { + return; + }; + + let corner_key = self.viewctx.corner.timestamp.as_ref().unwrap_or(last_key); + + if self.cursor < self.viewctx.corner { + // Cursor is above the viewport; move it inside. + self.cursor = corner_key.clone().into(); + } + + // Check whether the cursor is below the viewport. + let mut lines = 0; + + let cursor_key = self.cursor.timestamp.as_ref().unwrap_or(last_key); + + for (idx, item) in info.messages.range(corner_key.clone()..) { + if idx == cursor_key { + // Cursor is already within the viewport. + break; + } + + lines += item.show(false, &self.viewctx).height().max(1); + + if lines >= self.viewctx.get_height() { + // We've reached the end of the viewport; move cursor into it. + self.cursor = idx.clone().into(); + break; + } + } + } + + fn _range_to(&self, cursor: MessageCursor) -> EditRange { + EditRange::inclusive(self.cursor.clone(), cursor, TargetShape::LineWise) + } + + fn movement( + &self, + pos: MessageKey, + movement: &MoveType, + count: &Count, + ctx: &ProgramContext, + info: &RoomInfo, + ) -> Option { + let count = ctx.resolve(count); + + match movement { + // These movements don't map meaningfully onto the scrollback history. + MoveType::BufferByteOffset => None, + MoveType::Column(_, _) => None, + MoveType::ItemMatch => None, + MoveType::LineColumnOffset => None, + MoveType::LinePercent => None, + MoveType::LinePos(_) => None, + MoveType::SentenceBegin(_) => None, + MoveType::ScreenFirstWord(_) => None, + MoveType::ScreenLinePos(_) => None, + MoveType::WordBegin(_, _) => None, + MoveType::WordEnd(_, _) => None, + + MoveType::BufferLineOffset => None, + MoveType::BufferLinePercent => None, + MoveType::BufferPos(MovePosition::Beginning) => { + let start = info.messages.first_key_value()?.0.clone(); + + Some(start.into()) + }, + MoveType::BufferPos(MovePosition::Middle) => None, + MoveType::BufferPos(MovePosition::End) => Some(MessageCursor::latest()), + MoveType::FinalNonBlank(dir) | + MoveType::FirstWord(dir) | + MoveType::Line(dir) | + MoveType::ScreenLine(dir) | + MoveType::ParagraphBegin(dir) | + MoveType::SectionBegin(dir) | + MoveType::SectionEnd(dir) => { + match dir { + MoveDir1D::Previous => nth_before(pos, count, info).into(), + MoveDir1D::Next => nth_after(pos, count, info).into(), + } + }, + MoveType::ViewportPos(MovePosition::Beginning) => { + return self.viewctx.corner.timestamp.as_ref().map(|k| k.clone().into()); + }, + MoveType::ViewportPos(MovePosition::Middle) => { + // XXX: Need to calculate an accurate middle position. + return None; + }, + MoveType::ViewportPos(MovePosition::End) => { + // XXX: Need store to calculate an accurate end position. + return None; + }, + + _ => None, + } + } + + fn range_of_movement( + &self, + pos: MessageKey, + movement: &MoveType, + count: &Count, + ctx: &ProgramContext, + info: &RoomInfo, + ) -> Option> { + let other = self.movement(pos.clone(), movement, count, ctx, info)?; + + Some(EditRange::inclusive(pos.into(), other, TargetShape::LineWise)) + } + + fn range( + &self, + pos: MessageKey, + range: &RangeType, + _: bool, + count: &Count, + ctx: &ProgramContext, + info: &RoomInfo, + ) -> Option> { + match range { + RangeType::Bracketed(_, _) => None, + RangeType::Item => None, + RangeType::Quote(_) => None, + RangeType::Word(_) => None, + RangeType::XmlTag => None, + + RangeType::Buffer => { + let start = info.messages.first_key_value()?.0.clone(); + let end = info.messages.last_key_value()?.0.clone(); + + Some(EditRange::inclusive(start.into(), end.into(), TargetShape::LineWise)) + }, + RangeType::Line | RangeType::Paragraph | RangeType::Sentence => { + let count = ctx.resolve(count); + + if count == 0 { + return None; + } + + let mut end = &pos; + + for (i, (key, _)) in info.messages.range(&pos..).enumerate() { + if i >= count { + break; + } + + end = key; + } + + let end = end.clone().into(); + let start = pos.into(); + + Some(EditRange::inclusive(start, end, TargetShape::LineWise)) + }, + + _ => None, + } + } + + fn find_message_next( + &self, + start: MessageKey, + needle: &Regex, + mut count: usize, + info: &RoomInfo, + ) -> Option { + let mut mc = None; + + for (key, msg) in info.messages.range(&start..) { + if count == 0 { + break; + } + + if key == &start { + continue; + } + + if needle.is_match(msg.as_ref()) { + mc = MessageCursor::from(key.clone()).into(); + count -= 1; + } + } + + return mc; + } + + fn find_message_prev( + &self, + end: MessageKey, + needle: &Regex, + mut count: usize, + info: &RoomInfo, + need_load: &mut HashSet, + ) -> Option { + let mut mc = None; + + for (key, msg) in info.messages.range(..&end).rev() { + if count == 0 { + break; + } + + if needle.is_match(msg.as_ref()) { + mc = MessageCursor::from(key.clone()).into(); + count -= 1; + } + } + + if count > 0 { + need_load.insert(self.room_id.clone()); + } + + return mc; + } + + fn find_message( + &self, + key: MessageKey, + dir: MoveDir1D, + needle: &Regex, + count: usize, + info: &RoomInfo, + need_load: &mut HashSet, + ) -> Option { + match dir { + MoveDir1D::Next => self.find_message_next(key, needle, count, info), + MoveDir1D::Previous => self.find_message_prev(key, needle, count, info, need_load), + } + } +} + +impl WindowOps for ScrollbackState { + fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { + Scrollback::new(store).focus(focused).render(area, buf, self) + } + + fn dup(&self, _: &mut ProgramStore) -> Self { + ScrollbackState { + room_id: self.room_id.clone(), + id: self.id.clone(), + cursor: self.cursor.clone(), + viewctx: self.viewctx.clone(), + jumped: self.jumped.clone(), + } + } + + fn close(&mut self, _: CloseFlags, _: &mut ProgramStore) -> bool { + // XXX: what's the right closing behaviour for a room? + // Should write send a message? + true + } + + fn get_cursor_word(&self, _: &WordStyle) -> Option { + None + } + + fn get_selected_word(&self) -> Option { + None + } +} + +impl EditorActions for ScrollbackState { + fn edit( + &mut self, + operation: &EditAction, + motion: &EditTarget, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + let info = store.application.rooms.entry(self.room_id.clone()).or_default(); + let key = if let Some(k) = self.cursor.to_key(info) { + k.clone() + } else { + let msg = "No messages to select."; + let err = EditError::Failure(msg.to_string()); + return Err(err); + }; + + match operation { + EditAction::Motion => { + if motion.is_jumping() { + self.jumped.push(self.cursor.clone()); + } + + let pos = match motion { + EditTarget::CurrentPosition | EditTarget::Selection => { + return Ok(None); + }, + EditTarget::Boundary(rt, inc, term, count) => { + self.range(key, rt, *inc, count, ctx, info).map(|r| { + match term { + MoveTerminus::Beginning => r.start, + MoveTerminus::End => r.end, + } + }) + }, + EditTarget::CharJump(mark) | EditTarget::LineJump(mark) => { + let mark = ctx.resolve(mark); + let cursor = store.cursors.get_mark(self.id.clone(), mark)?; + + if let mc @ Some(_) = MessageCursor::from_cursor(&cursor, info) { + mc + } else { + let msg = "Failed to restore mark"; + let err = EditError::Failure(msg.into()); + + return Err(err); + } + }, + EditTarget::Motion(mt, count) => self.movement(key, mt, count, ctx, info), + EditTarget::Range(_, _, _) => { + return Err(EditError::Failure("Cannot use ranges in a list".to_string())); + }, + EditTarget::Search(SearchType::Char(_), _, _) => { + let msg = "Cannot perform character search in a list"; + let err = EditError::Failure(msg.into()); + + return Err(err); + }, + EditTarget::Search(SearchType::Regex, flip, count) => { + let count = ctx.resolve(count); + + let dir = ctx.get_search_regex_dir(); + let dir = flip.resolve(&dir); + + let needle = match ctx.get_search_regex() { + Some(re) => re, + None => { + let lsearch = store.registers.get(&Register::LastSearch); + let lsearch = lsearch.value.to_string(); + + Regex::new(lsearch.as_ref())? + }, + }; + + self.find_message( + key, + dir, + &needle, + count, + info, + &mut store.application.need_load, + ) + }, + EditTarget::Search(SearchType::Word(_, _), _, _) => { + let msg = "Cannot perform word search in a list"; + let err = EditError::Failure(msg.into()); + + return Err(err); + }, + + _ => { + let msg = format!("Unknown editing target: {:?}", motion); + let err = EditError::Unimplemented(msg); + + return Err(err); + }, + }; + + if let Some(pos) = pos { + self.cursor = pos; + } + + return Ok(None); + }, + EditAction::Yank => { + let range = match motion { + EditTarget::CurrentPosition | EditTarget::Selection => { + Some(self._range_to(key.into())) + }, + EditTarget::Boundary(rt, inc, term, count) => { + self.range(key, rt, *inc, count, ctx, info).map(|r| { + self._range_to(match term { + MoveTerminus::Beginning => r.start, + MoveTerminus::End => r.end, + }) + }) + }, + EditTarget::CharJump(mark) | EditTarget::LineJump(mark) => { + let mark = ctx.resolve(mark); + let cursor = store.cursors.get_mark(self.id.clone(), mark)?; + + if let Some(c) = MessageCursor::from_cursor(&cursor, info) { + self._range_to(c).into() + } else { + let msg = "Failed to restore mark"; + let err = EditError::Failure(msg.into()); + + return Err(err); + } + }, + EditTarget::Motion(mt, count) => { + self.range_of_movement(key, mt, count, ctx, info) + }, + EditTarget::Range(rt, inc, count) => { + self.range(key, rt, *inc, count, ctx, info) + }, + EditTarget::Search(SearchType::Char(_), _, _) => { + let msg = "Cannot perform character search in a list"; + let err = EditError::Failure(msg.into()); + + return Err(err); + }, + EditTarget::Search(SearchType::Regex, flip, count) => { + let count = ctx.resolve(count); + + let dir = ctx.get_search_regex_dir(); + let dir = flip.resolve(&dir); + + let needle = match ctx.get_search_regex() { + Some(re) => re, + None => { + let lsearch = store.registers.get(&Register::LastSearch); + let lsearch = lsearch.value.to_string(); + + Regex::new(lsearch.as_ref())? + }, + }; + + self.find_message( + key, + dir, + &needle, + count, + info, + &mut store.application.need_load, + ) + .map(|c| self._range_to(c)) + }, + EditTarget::Search(SearchType::Word(_, _), _, _) => { + let msg = "Cannot perform word search in a list"; + let err = EditError::Failure(msg.into()); + + return Err(err); + }, + + _ => { + let msg = format!("Unknown motion: {:?}", motion); + let err = EditError::Unimplemented(msg); + + return Err(err); + }, + }; + + if let Some(range) = range { + let mut yanked = EditRope::from(""); + + for (_, msg) in self.messages(range, info) { + yanked += EditRope::from(msg.as_ref()); + yanked += EditRope::from('\n'); + } + + let cell = RegisterCell::new(TargetShape::LineWise, yanked); + let register = ctx.get_register().unwrap_or(Register::Unnamed); + let mut flags = RegisterPutFlags::NONE; + + if ctx.get_register_append() { + flags |= RegisterPutFlags::APPEND; + } + + store.registers.put(®ister, cell, flags); + } + + return Ok(None); + }, + + // Everything else is a modifying action. + EditAction::ChangeCase(_) => Err(EditError::ReadOnly), + EditAction::ChangeNumber(_) => Err(EditError::ReadOnly), + EditAction::Delete => Err(EditError::ReadOnly), + EditAction::Format => Err(EditError::ReadOnly), + EditAction::Indent(_) => Err(EditError::ReadOnly), + EditAction::Join(_) => Err(EditError::ReadOnly), + EditAction::Replace(_) => Err(EditError::ReadOnly), + } + } + + fn mark( + &mut self, + name: Mark, + _: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + let info = store.application.get_room_info(self.room_id.clone()); + + if let Some(cursor) = self.cursor.to_cursor(info) { + store.cursors.set_mark(self.id.clone(), name, cursor); + + Ok(None) + } else { + let msg = "Failed to set mark for message"; + let err = EditError::Failure(msg.into()); + + Err(err) + } + } + + fn insert_text( + &mut self, + _: &InsertTextAction, + _: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult { + Err(EditError::ReadOnly) + } + + fn selection_command( + &mut self, + _: &SelectionAction, + _: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult { + Err(EditError::Failure("Cannot perform selection actions in a list".into())) + } + + fn history_command( + &mut self, + act: &HistoryAction, + _: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult { + match act { + HistoryAction::Checkpoint => Ok(None), + HistoryAction::Undo(_) => Err(EditError::Failure("Nothing to undo".into())), + HistoryAction::Redo(_) => Err(EditError::Failure("Nothing to redo".into())), + _ => Err(EditError::Unimplemented(format!("Unknown action: {:?}", act))), + } + } + + fn cursor_command( + &mut self, + act: &CursorAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + let info = store.application.get_room_info(self.room_id.clone()); + + match act { + CursorAction::Close(_) => Ok(None), + CursorAction::Rotate(_, _) => Ok(None), + CursorAction::Split(_) => Ok(None), + + CursorAction::Restore(_) => { + let reg = ctx.get_register().unwrap_or(Register::UnnamedCursorGroup); + + // Get saved group. + let ngroup = store.cursors.get_group(self.id.clone(), ®)?; + + // Lists don't have groups; override current position. + if self.jumped.current() != &self.cursor { + self.jumped.push(self.cursor.clone()); + } + + if let Some(mc) = MessageCursor::from_cursor(ngroup.leader.cursor(), info) { + self.cursor = mc; + + Ok(None) + } else { + let msg = "Cannot restore position in message history"; + let err = EditError::Failure(msg.into()); + + Err(err) + } + }, + CursorAction::Save(_) => { + let reg = ctx.get_register().unwrap_or(Register::UnnamedCursorGroup); + + // Lists don't have groups; override any previously saved group. + let cursor = self.cursor.to_cursor(info).ok_or_else(|| { + let msg = "Cannot save position in message history"; + EditError::Failure(msg.into()) + })?; + + let group = CursorGroup { + leader: CursorState::Location(cursor), + members: vec![], + }; + + store.cursors.set_group(self.id.clone(), reg, group)?; + + Ok(None) + }, + _ => Err(EditError::Unimplemented(format!("Unknown action: {:?}", act))), + } + } +} + +impl Editable for ScrollbackState { + fn editor_command( + &mut self, + act: &EditorAction, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + match act { + EditorAction::Cursor(act) => self.cursor_command(act, ctx, store), + EditorAction::Edit(ea, et) => self.edit(&ctx.resolve(ea), et, ctx, store), + EditorAction::History(act) => self.history_command(act, ctx, store), + EditorAction::InsertText(act) => self.insert_text(act, ctx, store), + EditorAction::Mark(name) => self.mark(ctx.resolve(name), ctx, store), + EditorAction::Selection(act) => self.selection_command(act, ctx, store), + + EditorAction::Complete(_, _) => { + let msg = ""; + let err = EditError::Unimplemented(msg.into()); + + Err(err) + }, + + _ => Err(EditError::Unimplemented(format!("Unknown action: {:?}", act))), + } + } +} + +impl Jumpable for ScrollbackState { + fn jump( + &mut self, + list: PositionList, + dir: MoveDir1D, + count: usize, + _: &ProgramContext, + ) -> UIResult { + match list { + PositionList::ChangeList => { + let msg = "No changes to jump to within the list"; + let err = UIError::Failure(msg.into()); + + return Err(err); + }, + PositionList::JumpList => { + let (len, pos) = match dir { + MoveDir1D::Previous => { + if self.jumped.future_len() == 0 && *self.jumped.current() != self.cursor { + // Push current position if this is the first jump backwards. + self.jumped.push(self.cursor.clone()); + } + + let plen = self.jumped.past_len(); + let pos = self.jumped.prev(count); + + (plen, pos) + }, + MoveDir1D::Next => { + let flen = self.jumped.future_len(); + let pos = self.jumped.next(count); + + (flen, pos) + }, + }; + + if len > 0 { + self.cursor = pos.clone(); + } + + return Ok(count.saturating_sub(len)); + }, + } + } +} + +impl Promptable for ScrollbackState { + fn prompt( + &mut self, + act: &PromptAction, + _: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult, ProgramContext)>, IambInfo> { + let info = store.application.get_room_info(self.room_id.clone()); + + let _ = if let Some(key) = self.cursor.to_key(info) { + key + } else { + let msg = "No message currently selected"; + let err = EditError::Failure(msg.into()); + + return Err(err); + }; + + match act { + PromptAction::Submit => { + // XXX: I'm not sure exactly what to do here yet. I think I want this to display a + // pop-over ListState with actions that can then be submitted: + // + // - Create a reply + // - Edit a message + // - Redact a message + // - React to a message + // - Report a message + // - Download an attachment + // + // Each of these should correspond to a command that a user can run. For example, + // running `:reply` when hovering over a message should be equivalent to opening + // the pop-up and selecting "Reply To This Message". + return Ok(vec![]); + }, + PromptAction::Abort(..) => { + let msg = "Cannot abort a message."; + let err = EditError::Failure(msg.into()); + + return Err(err); + }, + PromptAction::Recall(..) => { + let msg = "Cannot recall previous messages."; + let err = EditError::Failure(msg.into()); + + return Err(err); + }, + _ => { + let msg = format!("Messages scrollback doesn't support {:?}", act); + let err = EditError::Unimplemented(msg); + + return Err(err); + }, + } + } +} + +impl ScrollActions for ScrollbackState { + fn dirscroll( + &mut self, + dir: MoveDir2D, + size: ScrollSize, + count: &Count, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + let info = store.application.get_room_info(self.room_id.clone()); + let mut corner = self.viewctx.corner.clone(); + + let last_key = if let Some(k) = info.messages.last_key_value() { + k.0 + } else { + return Ok(None); + }; + + let corner_key = corner.timestamp.as_ref().unwrap_or(last_key).clone(); + let cursor_key = self.cursor.timestamp.as_ref().unwrap_or(last_key); + + let count = ctx.resolve(count); + let height = self.viewctx.get_height(); + let mut rows = match size { + ScrollSize::Cell => count, + ScrollSize::HalfPage => count.saturating_mul(height) / 2, + ScrollSize::Page => count.saturating_mul(height), + }; + + match dir { + MoveDir2D::Up => { + let first_key = info.messages.first_key_value().map(|f| f.0.clone()); + + for (key, item) in info.messages.range(..=&corner_key).rev() { + let sel = key == cursor_key; + let txt = item.show(sel, &self.viewctx); + let len = txt.height().max(1); + let max = len.saturating_sub(1); + + if key != &corner_key { + corner.text_row = max; + } + + corner.timestamp = key.clone().into(); + + if rows == 0 { + break; + } else if corner.text_row >= rows { + corner.text_row -= rows; + break; + } else if corner.timestamp == first_key { + corner.text_row = 0; + break; + } + + rows -= corner.text_row + 1; + } + }, + MoveDir2D::Down => { + for (key, item) in info.messages.range(&corner_key..) { + let sel = key == cursor_key; + let txt = item.show(sel, &self.viewctx); + let len = txt.height().max(1); + let max = len.saturating_sub(1); + + if key != &corner_key { + corner.text_row = 0; + } + + corner.timestamp = key.clone().into(); + + if rows == 0 { + break; + } else if key == last_key { + corner.text_row = corner.text_row.saturating_add(rows).min(max); + break; + } else if corner.text_row >= max { + rows -= 1; + continue; + } else if corner.text_row + rows <= max { + corner.text_row += rows; + break; + } else { + rows -= len - corner.text_row; + continue; + } + } + }, + MoveDir2D::Left | MoveDir2D::Right => { + let msg = "Cannot scroll vertically in message scrollback"; + let err = EditError::Failure(msg.into()); + + return Err(err); + }, + } + + self.viewctx.corner = corner; + self.shift_cursor(info); + + Ok(None) + } + + fn cursorpos( + &mut self, + pos: MovePosition, + axis: Axis, + _: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + match axis { + Axis::Horizontal => { + let msg = "Cannot scroll vertically in message scrollback"; + let err = EditError::Failure(msg.into()); + + Err(err) + }, + Axis::Vertical => { + let info = store.application.get_room_info(self.room_id.clone()); + + if let Some(key) = self.cursor.to_key(info).cloned() { + self.scrollview(key, pos, info); + } + + Ok(None) + }, + } + } + + fn linepos( + &mut self, + _: MovePosition, + _: &Count, + _: &ProgramContext, + _: &mut ProgramStore, + ) -> EditResult { + let msg = "Cannot scroll in message scrollback using line numbers"; + let err = EditError::Failure(msg.into()); + + Err(err) + } +} + +impl Scrollable for ScrollbackState { + fn scroll( + &mut self, + style: &ScrollStyle, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> EditResult { + match style { + ScrollStyle::Direction2D(dir, size, count) => { + return self.dirscroll(*dir, *size, count, ctx, store); + }, + ScrollStyle::CursorPos(pos, axis) => { + return self.cursorpos(*pos, *axis, ctx, store); + }, + ScrollStyle::LinePos(pos, count) => { + return self.linepos(*pos, count, ctx, store); + }, + } + } +} + +impl Searchable for ScrollbackState { + fn search( + &mut self, + dir: MoveDirMod, + count: Count, + ctx: &ProgramContext, + store: &mut ProgramStore, + ) -> UIResult { + let search = EditTarget::Search(SearchType::Regex, dir, count); + + Ok(self.edit(&EditAction::Motion, &search, ctx, store)?) + } +} + +impl TerminalCursor for ScrollbackState { + fn get_term_cursor(&self) -> Option<(u16, u16)> { + None + } +} + +pub struct Scrollback<'a> { + focused: bool, + store: &'a mut ProgramStore, +} + +impl<'a> Scrollback<'a> { + pub fn new(store: &'a mut ProgramStore) -> Self { + Scrollback { focused: false, store } + } + + /// Indicate whether the scrollback is currently focused. + pub fn focus(mut self, focused: bool) -> Self { + self.focused = focused; + self + } +} + +impl<'a> StatefulWidget for Scrollback<'a> { + type State = ScrollbackState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + state.set_term_info(area); + + let height = state.viewctx.get_height(); + + if height == 0 { + return; + } + + if state.cursor.timestamp < state.viewctx.corner.timestamp { + state.viewctx.corner = state.cursor.clone(); + } + + let info = self.store.application.get_room_info(state.room_id.clone()); + + let cursor = &state.cursor; + let cursor_key = if let Some(k) = cursor.to_key(info) { + k + } else { + self.store.application.mark_for_load(state.room_id.clone()); + return; + }; + + let corner = &state.viewctx.corner; + let corner_key = match (&corner.timestamp, &cursor.timestamp) { + (_, None) => nth_key_before(cursor_key.clone(), height, info), + (None, _) => nth_key_before(cursor_key.clone(), height, info), + (Some(k), _) => k.clone(), + }; + + let mut lines = vec![]; + let mut sawit = false; + + for (key, item) in info.messages.range(&corner_key..) { + let sel = key == cursor_key; + let txt = item.show(self.focused && sel, &state.viewctx); + + for (row, line) in txt.lines.into_iter().enumerate() { + if sawit && lines.len() >= height { + // Check whether we've seen the first line of the + // selected message and can fill the screen. + break; + } + + if key == &corner_key && row < corner.text_row { + // Skip rows above the viewport corner. + continue; + } + + lines.push((key, row, line)); + sawit |= sel; + } + } + + if lines.len() > height { + let n = lines.len() - height; + let _ = lines.drain(..n); + } + + if let Some(((ts, event_id), row, _)) = lines.first() { + state.viewctx.corner.timestamp = Some((*ts, event_id.clone())); + state.viewctx.corner.text_row = *row; + } + + let mut y = area.top(); + let x = area.left(); + + for (_, _, txt) in lines.into_iter() { + let _ = buf.set_spans(x, y, &txt, area.width); + + y += 1; + } + + let first_key = info.messages.first_key_value().map(|f| f.0.clone()); + if first_key == state.viewctx.corner.timestamp { + // If the top of the screen is the older message, load more. + self.store.application.mark_for_load(state.room_id.clone()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test_search_messages() { + let room_id = TEST_ROOM1_ID.clone(); + let mut store = mock_store(); + let mut scrollback = ScrollbackState::new(room_id.clone()); + let ctx = ProgramContext::default(); + + let next = MoveDirMod::Exact(MoveDir1D::Next); + let prev = MoveDirMod::Exact(MoveDir1D::Previous); + + // Search through the messages: + // + // MSG2: "helium" + // MSG3: "this\nis\na\nmultiline\nmessage" + // MSG4: "help" + // MSG5: "character" + // MSG1: "writhe" + store.set_last_search("he"); + + assert_eq!(scrollback.cursor, MessageCursor::latest()); + + // Search backwards to MSG4. + scrollback.search(prev.clone(), 1.into(), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG4_KEY.clone().into()); + + // Search backwards to MSG2. + scrollback.search(prev.clone(), 1.into(), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG2_KEY.clone().into()); + assert_eq!(store.application.need_load.contains(&room_id), false); + + // Can't go any further; need_load now contains the room ID. + scrollback.search(prev.clone(), 1.into(), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG2_KEY.clone().into()); + assert_eq!(store.application.need_load.contains(&room_id), true); + + // Search forward twice to MSG1. + scrollback.search(next.clone(), 2.into(), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG1_KEY.clone().into()); + + // Can't go any further. + scrollback.search(next.clone(), 2.into(), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG1_KEY.clone().into()); + } + + #[test] + fn test_movement() { + let mut store = mock_store(); + let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone()); + let ctx = ProgramContext::default(); + + let prev = |n: usize| EditTarget::Motion(MoveType::Line(MoveDir1D::Previous), n.into()); + + let next = |n: usize| EditTarget::Motion(MoveType::Line(MoveDir1D::Next), n.into()); + + assert_eq!(scrollback.cursor, MessageCursor::latest()); + + scrollback.edit(&EditAction::Motion, &prev(1), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG5_KEY.clone().into()); + + scrollback.edit(&EditAction::Motion, &prev(2), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG3_KEY.clone().into()); + + scrollback.edit(&EditAction::Motion, &prev(5), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG2_KEY.clone().into()); + + scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG3_KEY.clone().into()); + + scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG4_KEY.clone().into()); + + scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG5_KEY.clone().into()); + + scrollback.edit(&EditAction::Motion, &next(1), &ctx, &mut store).unwrap(); + assert_eq!(scrollback.cursor, MSG1_KEY.clone().into()); + } + + #[test] + fn test_dirscroll() { + let mut store = mock_store(); + let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone()); + let ctx = ProgramContext::default(); + + let prev = MoveDir2D::Up; + let next = MoveDir2D::Down; + + assert_eq!(scrollback.cursor, MessageCursor::latest()); + assert_eq!(scrollback.viewctx.dimensions, (0, 0)); + assert_eq!(scrollback.viewctx.corner, MessageCursor::latest()); + + // Set a terminal width of 60, and height of 3, rendering in scrollback as: + // + // |------------------------------------------------------------| + // MSG2: | @user2:example.com helium | + // MSG3: | @user2:example.com this | + // | is | + // | a | + // | multiline | + // | message | + // MSG4: | @user1:example.com help | + // MSG5: | @user2:example.com character | + // MSG1: | @user1:example.com writhe | + // |------------------------------------------------------------| + let area = Rect::new(0, 0, 60, 3); + let mut buffer = Buffer::empty(area); + scrollback.draw(area, &mut buffer, true, &mut store); + + assert_eq!(scrollback.cursor, MessageCursor::latest()); + assert_eq!(scrollback.viewctx.dimensions, (60, 3)); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG4_KEY.clone(), 0)); + + // Scroll up a line at a time until we hit the first message. + scrollback + .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4)); + + scrollback + .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 3)); + + scrollback + .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 2)); + + scrollback + .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 1)); + + scrollback + .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 0)); + + scrollback + .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 0)); + + // Cannot scroll any further. + scrollback + .dirscroll(prev, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG2_KEY.clone(), 0)); + + // Now scroll back down one line at a time. + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 0)); + + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 1)); + + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 2)); + + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 3)); + + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4)); + + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG4_KEY.clone(), 0)); + + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG5_KEY.clone(), 0)); + + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 0)); + + // Cannot scroll down any further. + scrollback + .dirscroll(next, ScrollSize::Cell, &1.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG1_KEY.clone(), 0)); + + // Scroll up two Pages (six lines). + scrollback + .dirscroll(prev, ScrollSize::Page, &2.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 1)); + + // Scroll down two HalfPages (three lines). + scrollback + .dirscroll(next, ScrollSize::HalfPage, &2.into(), &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4)); + } + + #[test] + fn test_cursorpos() { + let mut store = mock_store(); + let mut scrollback = ScrollbackState::new(TEST_ROOM1_ID.clone()); + let ctx = ProgramContext::default(); + + // Set a terminal width of 60, and height of 3, rendering in scrollback as: + // + // |------------------------------------------------------------| + // MSG2: | @user2:example.com helium | + // MSG3: | @user2:example.com this | + // | is | + // | a | + // | multiline | + // | message | + // MSG4: | @user1:example.com help | + // MSG5: | @user2:example.com character | + // MSG1: | @user1:example.com writhe | + // |------------------------------------------------------------| + let area = Rect::new(0, 0, 60, 3); + let mut buffer = Buffer::empty(area); + scrollback.cursor = MSG4_KEY.clone().into(); + scrollback.draw(area, &mut buffer, true, &mut store); + + assert_eq!(scrollback.cursor, MSG4_KEY.clone().into()); + assert_eq!(scrollback.viewctx.dimensions, (60, 3)); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 3)); + + // Scroll so that the cursor is at the top of the screen. + scrollback + .cursorpos(MovePosition::Beginning, Axis::Vertical, &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.cursor, MSG4_KEY.clone().into()); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG4_KEY.clone(), 0)); + + // Scroll so that the cursor is at the bottom of the screen. + scrollback + .cursorpos(MovePosition::End, Axis::Vertical, &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.cursor, MSG4_KEY.clone().into()); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 3)); + + // Scroll so that the cursor is in the middle of the screen. + scrollback + .cursorpos(MovePosition::Middle, Axis::Vertical, &ctx, &mut store) + .unwrap(); + assert_eq!(scrollback.cursor, MSG4_KEY.clone().into()); + assert_eq!(scrollback.viewctx.corner, MessageCursor::new(MSG3_KEY.clone(), 4)); + } +} diff --git a/src/windows/room/space.rs b/src/windows/room/space.rs new file mode 100644 index 0000000..9a562aa --- /dev/null +++ b/src/windows/room/space.rs @@ -0,0 +1,105 @@ +use std::ops::{Deref, DerefMut}; + +use matrix_sdk::{ + room::Room as MatrixRoom, + ruma::{OwnedRoomId, RoomId}, +}; + +use modalkit::tui::{buffer::Buffer, layout::Rect, widgets::StatefulWidget}; + +use modalkit::{ + widgets::list::{List, ListState}, + widgets::{TermOffset, TerminalCursor, WindowOps}, +}; + +use crate::base::{IambBufferId, IambInfo, ProgramStore, RoomFocus}; + +use crate::windows::RoomItem; + +pub struct SpaceState { + room_id: OwnedRoomId, + list: ListState, +} + +impl SpaceState { + pub fn new(room: MatrixRoom) -> Self { + let room_id = room.room_id().to_owned(); + let content = IambBufferId::Room(room_id.clone(), RoomFocus::Scrollback); + let list = ListState::new(content, vec![]); + + SpaceState { room_id, list } + } + + pub fn id(&self) -> &RoomId { + &self.room_id + } + + pub fn dup(&self, store: &mut ProgramStore) -> Self { + SpaceState { + room_id: self.room_id.clone(), + list: self.list.dup(store), + } + } +} + +impl TerminalCursor for SpaceState { + fn get_term_cursor(&self) -> Option { + self.list.get_term_cursor() + } +} + +impl Deref for SpaceState { + type Target = ListState; + + fn deref(&self) -> &Self::Target { + &self.list + } +} + +impl DerefMut for SpaceState { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.list + } +} + +pub struct Space<'a> { + focused: bool, + store: &'a mut ProgramStore, +} + +impl<'a> Space<'a> { + pub fn new(store: &'a mut ProgramStore) -> Self { + Space { focused: false, store } + } + + pub fn focus(mut self, focused: bool) -> Self { + self.focused = focused; + self + } +} + +impl<'a> StatefulWidget for Space<'a> { + type State = SpaceState; + + fn render(self, area: Rect, buffer: &mut Buffer, state: &mut Self::State) { + let members = self.store.application.worker.space_members(state.room_id.clone()).unwrap(); + let items = members + .into_iter() + .filter_map(|id| { + let (room, name) = self.store.application.worker.get_room(id.clone()).ok()?; + + if id != state.room_id { + Some(RoomItem::new(room, name, self.store)) + } else { + None + } + }) + .collect(); + + state.list.set(items); + + List::new(self.store) + .focus(self.focused) + .render(area, buffer, &mut state.list) + } +} diff --git a/src/windows/welcome.md b/src/windows/welcome.md new file mode 100644 index 0000000..f3a1f50 --- /dev/null +++ b/src/windows/welcome.md @@ -0,0 +1,44 @@ +# Welcome to iamb! + +## Useful Keybindings + +- `` will send a typed message +- `O`/`o` can be used to insert blank lines before and after the cursor line +- `^O` can be used in Insert mode to enter a single Normal mode keybinding sequence +- `^Wm` can be used to toggle whether the message bar or scrollback is selected +- `^Wz` can be used to toggle whether the current window takes up the full screen + +## Room Commands + +- `:dms` will open a list of direct messages +- `:rooms` will open a list of joined rooms +- `:spaces` will open a list of joined spaces +- `:join` can be used to switch to join a new room or start a direct message +- `:split` and `:vsplit` can be used to open rooms in a new window + +## Verification Commands + +The `:verify` command has several different subcommands for working with +verification requests. When used without any arguments, it will take you to a +list of current verifications, where you can see and compare the Emoji. + +The different subcommands are: + +- `:verify request USERNAME` will send a verification request to a user +- `:verify confirm USERNAME/DEVICE` will confirm a verification +- `:verify mismatch USERNAME/DEVICE` will cancel a verification where the Emoji don't match +- `:verify cancel USERNAME/DEVICE` will cancel a verification + +## Other Useful Commands + +- `:welcome` will take you back to this screen + +## Additional Configuration + +You can customize iamb in your `$CONFIG_DIR/iamb/config.json` file, where +`$CONFIG_DIR` is your system's per-user configuration directory. + +You can edit the following values in the file: + +- `"default_profile"`, a profile name to use when starting iamb if one wasn't specified +- `"cache"`, a directory for cached iamb diff --git a/src/windows/welcome.rs b/src/windows/welcome.rs new file mode 100644 index 0000000..8d17212 --- /dev/null +++ b/src/windows/welcome.rs @@ -0,0 +1,71 @@ +use std::ops::{Deref, DerefMut}; + +use modalkit::tui::{buffer::Buffer, layout::Rect}; + +use modalkit::{ + widgets::textbox::TextBoxState, + widgets::WindowOps, + widgets::{TermOffset, TerminalCursor}, +}; + +use modalkit::editing::base::{CloseFlags, WordStyle}; + +use crate::base::{IambBufferId, IambInfo, ProgramStore}; + +const WELCOME_TEXT: &str = include_str!("welcome.md"); + +pub struct WelcomeState { + tbox: TextBoxState, +} + +impl WelcomeState { + pub fn new(store: &mut ProgramStore) -> Self { + let buf = store.buffers.load_str(IambBufferId::Welcome, WELCOME_TEXT); + + WelcomeState { tbox: TextBoxState::new(buf) } + } +} + +impl Deref for WelcomeState { + type Target = TextBoxState; + + fn deref(&self) -> &Self::Target { + return &self.tbox; + } +} + +impl DerefMut for WelcomeState { + fn deref_mut(&mut self) -> &mut Self::Target { + return &mut self.tbox; + } +} + +impl TerminalCursor for WelcomeState { + fn get_term_cursor(&self) -> Option { + self.tbox.get_term_cursor() + } +} + +impl WindowOps for WelcomeState { + fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut ProgramStore) { + self.tbox.draw(area, buf, focused, store) + } + + fn dup(&self, store: &mut ProgramStore) -> Self { + let tbox = self.tbox.dup(store); + + WelcomeState { tbox } + } + + fn close(&mut self, flags: CloseFlags, store: &mut ProgramStore) -> bool { + self.tbox.close(flags, store) + } + + fn get_cursor_word(&self, style: &WordStyle) -> Option { + self.tbox.get_cursor_word(style) + } + + fn get_selected_word(&self) -> Option { + self.tbox.get_selected_word() + } +} diff --git a/src/worker.rs b/src/worker.rs new file mode 100644 index 0000000..ad49d0a --- /dev/null +++ b/src/worker.rs @@ -0,0 +1,813 @@ +use std::convert::TryFrom; +use std::fs::File; +use std::io::BufWriter; +use std::str::FromStr; +use std::sync::mpsc::{sync_channel, Receiver, RecvTimeoutError, SyncSender}; +use std::sync::Arc; +use std::time::Duration; + +use gethostname::gethostname; +use tokio::task::JoinHandle; +use tracing::error; + +use matrix_sdk::{ + config::{RequestConfig, StoreConfig, SyncSettings}, + encryption::verification::{SasVerification, Verification}, + event_handler::Ctx, + reqwest, + room::{Messages, MessagesOptions, Room as MatrixRoom}, + ruma::{ + api::client::{ + room::create_room::v3::{Request as CreateRoomRequest, RoomPreset}, + room::Visibility, + space::get_hierarchy::v1::Request as SpaceHierarchyRequest, + }, + events::{ + key::verification::{ + done::{OriginalSyncKeyVerificationDoneEvent, ToDeviceKeyVerificationDoneEvent}, + key::{OriginalSyncKeyVerificationKeyEvent, ToDeviceKeyVerificationKeyEvent}, + request::ToDeviceKeyVerificationRequestEvent, + start::{OriginalSyncKeyVerificationStartEvent, ToDeviceKeyVerificationStartEvent}, + VerificationMethod, + }, + room::message::{MessageType, RoomMessageEventContent, TextMessageEventContent}, + room::name::RoomNameEventContent, + AnyMessageLikeEvent, + AnyTimelineEvent, + SyncMessageLikeEvent, + SyncStateEvent, + }, + OwnedEventId, + OwnedRoomId, + OwnedRoomOrAliasId, + OwnedUserId, + }, + Client, + DisplayName, + Session, +}; + +use modalkit::editing::action::{EditInfo, InfoMessage, UIError}; + +use crate::{ + base::{AsyncProgramStore, IambError, IambResult, VerifyAction}, + message::{Message, MessageFetchResult, MessageTimeStamp}, + ApplicationSettings, +}; + +const IAMB_DEVICE_NAME: &str = "iamb"; +const IAMB_USER_AGENT: &str = "iamb"; +const REQ_TIMEOUT: Duration = Duration::from_secs(60); + +fn initial_devname() -> String { + format!("{} on {}", IAMB_DEVICE_NAME, gethostname().to_string_lossy()) +} + +pub enum LoginStyle { + SessionRestore(Session), + Password(String), +} + +pub struct ClientResponse(Receiver); +pub struct ClientReply(SyncSender); + +impl ClientResponse { + fn recv(self) -> T { + self.0.recv().expect("failed to receive response from client thread") + } +} + +impl ClientReply { + fn send(self, t: T) { + self.0.send(t).unwrap(); + } +} + +fn oneshot() -> (ClientReply, ClientResponse) { + let (tx, rx) = sync_channel(1); + let reply = ClientReply(tx); + let response = ClientResponse(rx); + + return (reply, response); +} + +type EchoPair = (OwnedEventId, RoomMessageEventContent); + +pub enum WorkerTask { + DirectMessages(ClientReply>), + Init(AsyncProgramStore, ClientReply<()>), + LoadOlder(OwnedRoomId, Option, u32, ClientReply), + Login(LoginStyle, ClientReply>), + GetRoom(OwnedRoomId, ClientReply>), + JoinRoom(String, ClientReply>), + JoinedRooms(ClientReply>), + SpaceMembers(OwnedRoomId, ClientReply>>), + Spaces(ClientReply>), + SendMessage(OwnedRoomId, String, ClientReply>), + Verify(VerifyAction, SasVerification, ClientReply>), + VerifyRequest(OwnedUserId, ClientReply>), +} + +#[derive(Clone)] +pub struct Requester { + pub tx: SyncSender, +} + +impl Requester { + pub fn init(&self, store: AsyncProgramStore) { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::Init(store, reply)).unwrap(); + + return response.recv(); + } + + pub fn load_older( + &self, + room_id: OwnedRoomId, + fetch_id: Option, + limit: u32, + ) -> MessageFetchResult { + let (reply, response) = oneshot(); + + self.tx + .send(WorkerTask::LoadOlder(room_id, fetch_id, limit, reply)) + .unwrap(); + + return response.recv(); + } + + pub fn login(&self, style: LoginStyle) -> IambResult { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::Login(style, reply)).unwrap(); + + return response.recv(); + } + + pub fn send_message(&self, room_id: OwnedRoomId, msg: String) -> IambResult { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::SendMessage(room_id, msg, reply)).unwrap(); + + return response.recv(); + } + + pub fn direct_messages(&self) -> Vec<(MatrixRoom, DisplayName)> { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::DirectMessages(reply)).unwrap(); + + return response.recv(); + } + + pub fn get_room(&self, room_id: OwnedRoomId) -> IambResult<(MatrixRoom, DisplayName)> { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::GetRoom(room_id, reply)).unwrap(); + + return response.recv(); + } + + pub fn join_room(&self, name: String) -> IambResult { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::JoinRoom(name, reply)).unwrap(); + + return response.recv(); + } + + pub fn joined_rooms(&self) -> Vec<(MatrixRoom, DisplayName)> { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::JoinedRooms(reply)).unwrap(); + + return response.recv(); + } + + pub fn space_members(&self, space: OwnedRoomId) -> IambResult> { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::SpaceMembers(space, reply)).unwrap(); + + return response.recv(); + } + + pub fn spaces(&self) -> Vec<(MatrixRoom, DisplayName)> { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::Spaces(reply)).unwrap(); + + return response.recv(); + } + + pub fn verify(&self, act: VerifyAction, sas: SasVerification) -> IambResult { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::Verify(act, sas, reply)).unwrap(); + + return response.recv(); + } + + pub fn verify_request(&self, user_id: OwnedUserId) -> IambResult { + let (reply, response) = oneshot(); + + self.tx.send(WorkerTask::VerifyRequest(user_id, reply)).unwrap(); + + return response.recv(); + } +} + +pub struct ClientWorker { + initialized: bool, + settings: ApplicationSettings, + client: Client, + sync_handle: Option>, +} + +impl ClientWorker { + pub fn spawn(settings: ApplicationSettings) -> Requester { + let (tx, rx) = sync_channel(5); + + let _ = tokio::spawn(async move { + let account = &settings.profile; + + // Set up a custom client that only uses HTTP/1. + // + // During my testing, I kept stumbling across something weird with sync and HTTP/2 that + // will need to be revisited in the future. + let http = reqwest::Client::builder() + .user_agent(IAMB_USER_AGENT) + .timeout(Duration::from_secs(60)) + .pool_idle_timeout(Duration::from_secs(120)) + .pool_max_idle_per_host(5) + .http1_only() + .build() + .unwrap(); + + // Set up the Matrix client for the selected profile. + let client = Client::builder() + .http_client(Arc::new(http)) + .homeserver_url(account.url.clone()) + .store_config(StoreConfig::default()) + .sled_store(settings.matrix_dir.as_path(), None) + .expect("Failed to setup up sled store for Matrix SDK") + .request_config( + RequestConfig::new().timeout(REQ_TIMEOUT).retry_timeout(REQ_TIMEOUT), + ) + .build() + .await + .expect("Failed to instantiate Matrix client"); + + let mut worker = ClientWorker { + initialized: false, + settings, + client, + sync_handle: None, + }; + + worker.work(rx).await; + }); + + return Requester { tx }; + } + + async fn work(&mut self, rx: Receiver) { + loop { + let t = rx.recv_timeout(Duration::from_secs(1)); + + match t { + Ok(task) => self.run(task).await, + Err(RecvTimeoutError::Timeout) => {}, + Err(RecvTimeoutError::Disconnected) => { + break; + }, + } + } + + if let Some(handle) = self.sync_handle.take() { + handle.abort(); + } + } + + async fn run(&mut self, task: WorkerTask) { + match task { + WorkerTask::DirectMessages(reply) => { + assert!(self.initialized); + reply.send(self.direct_messages().await); + }, + WorkerTask::Init(store, reply) => { + assert_eq!(self.initialized, false); + self.init(store).await; + reply.send(()); + }, + WorkerTask::JoinRoom(room_id, reply) => { + assert!(self.initialized); + reply.send(self.join_room(room_id).await); + }, + WorkerTask::GetRoom(room_id, reply) => { + assert!(self.initialized); + reply.send(self.get_room(room_id).await); + }, + WorkerTask::JoinedRooms(reply) => { + assert!(self.initialized); + reply.send(self.joined_rooms().await); + }, + WorkerTask::LoadOlder(room_id, fetch_id, limit, reply) => { + assert!(self.initialized); + reply.send(self.load_older(room_id, fetch_id, limit).await); + }, + WorkerTask::Login(style, reply) => { + assert!(self.initialized); + reply.send(self.login_and_sync(style).await); + }, + WorkerTask::SpaceMembers(space, reply) => { + assert!(self.initialized); + reply.send(self.space_members(space).await); + }, + WorkerTask::Spaces(reply) => { + assert!(self.initialized); + reply.send(self.spaces().await); + }, + WorkerTask::SendMessage(room_id, msg, reply) => { + assert!(self.initialized); + reply.send(self.send_message(room_id, msg).await); + }, + WorkerTask::Verify(act, sas, reply) => { + assert!(self.initialized); + reply.send(self.verify(act, sas).await); + }, + WorkerTask::VerifyRequest(user_id, reply) => { + assert!(self.initialized); + reply.send(self.verify_request(user_id).await); + }, + } + } + + async fn init(&mut self, store: AsyncProgramStore) { + self.client.add_event_handler_context(store); + + let _ = self.client.add_event_handler( + |ev: SyncStateEvent, + room: MatrixRoom, + store: Ctx| { + async move { + if let SyncStateEvent::Original(ev) = ev { + if let Some(room_name) = ev.content.name { + let room_id = room.room_id().to_owned(); + let room_name = Some(room_name.to_string()); + let mut locked = store.lock().await; + let mut info = + locked.application.rooms.entry(room_id.to_owned()).or_default(); + info.name = room_name; + } + } + } + }, + ); + + let _ = self.client.add_event_handler( + |ev: SyncMessageLikeEvent, + room: MatrixRoom, + client: Client, + store: Ctx| { + async move { + let room_id = room.room_id(); + let room_name = room.display_name().await.ok(); + let room_name = room_name.as_ref().map(ToString::to_string); + + if let Some(msg) = ev.as_original() { + if let MessageType::VerificationRequest(_) = msg.content.msgtype { + if let Some(request) = client + .encryption() + .get_verification_request(ev.sender(), ev.event_id()) + .await + { + request.accept().await.expect("Failed to accept request"); + } + } + } + + let mut locked = store.lock().await; + let mut info = locked.application.get_room_info(room_id.to_owned()); + info.name = room_name; + + let event_id = ev.event_id().to_owned(); + let key = (ev.origin_server_ts().into(), event_id.clone()); + let msg = Message::from(ev.into_full_event(room_id.to_owned())); + info.messages.insert(key, msg); + + // Remove the echo. + let key = (MessageTimeStamp::LocalEcho, event_id); + let _ = info.messages.remove(&key); + } + }, + ); + + let _ = self.client.add_event_handler( + |ev: OriginalSyncKeyVerificationStartEvent, + client: Client, + store: Ctx| { + async move { + let tx_id = ev.content.relates_to.event_id.as_ref(); + + if let Some(Verification::SasV1(sas)) = + client.encryption().get_verification(&ev.sender, tx_id).await + { + sas.accept().await.unwrap(); + + store.lock().await.application.insert_sas(sas) + } + } + }, + ); + + let _ = self.client.add_event_handler( + |ev: OriginalSyncKeyVerificationKeyEvent, + client: Client, + store: Ctx| { + async move { + let tx_id = ev.content.relates_to.event_id.as_ref(); + + if let Some(Verification::SasV1(sas)) = + client.encryption().get_verification(&ev.sender, tx_id).await + { + store.lock().await.application.insert_sas(sas); + } + } + }, + ); + + let _ = self.client.add_event_handler( + |ev: OriginalSyncKeyVerificationDoneEvent, + client: Client, + store: Ctx| { + async move { + let tx_id = ev.content.relates_to.event_id.as_ref(); + + if let Some(Verification::SasV1(sas)) = + client.encryption().get_verification(&ev.sender, tx_id).await + { + store.lock().await.application.insert_sas(sas); + } + } + }, + ); + + let _ = self.client.add_event_handler( + |ev: ToDeviceKeyVerificationRequestEvent, client: Client| { + async move { + let request = client + .encryption() + .get_verification_request(&ev.sender, &ev.content.transaction_id) + .await + .unwrap(); + + request.accept().await.unwrap(); + } + }, + ); + + let _ = self.client.add_event_handler( + |ev: ToDeviceKeyVerificationStartEvent, + client: Client, + store: Ctx| { + async move { + let tx_id = ev.content.transaction_id; + + if let Some(Verification::SasV1(sas)) = + client.encryption().get_verification(&ev.sender, tx_id.as_ref()).await + { + sas.accept().await.unwrap(); + + store.lock().await.application.insert_sas(sas); + } + } + }, + ); + + let _ = self.client.add_event_handler( + |ev: ToDeviceKeyVerificationKeyEvent, client: Client, store: Ctx| { + async move { + let tx_id = ev.content.transaction_id; + + if let Some(Verification::SasV1(sas)) = + client.encryption().get_verification(&ev.sender, tx_id.as_ref()).await + { + store.lock().await.application.insert_sas(sas); + } + } + }, + ); + + let _ = self.client.add_event_handler( + |ev: ToDeviceKeyVerificationDoneEvent, + client: Client, + store: Ctx| { + async move { + let tx_id = ev.content.transaction_id; + + if let Some(Verification::SasV1(sas)) = + client.encryption().get_verification(&ev.sender, tx_id.as_ref()).await + { + store.lock().await.application.insert_sas(sas); + } + } + }, + ); + + self.initialized = true; + } + + async fn login_and_sync(&mut self, style: LoginStyle) -> IambResult { + let client = self.client.clone(); + + match style { + LoginStyle::SessionRestore(session) => { + client.restore_login(session).await.map_err(IambError::from)?; + }, + LoginStyle::Password(password) => { + let resp = client + .login_username(&self.settings.profile.user_id, &password) + .initial_device_display_name(initial_devname().as_str()) + .send() + .await + .map_err(IambError::from)?; + let file = File::create(self.settings.session_json.as_path())?; + let writer = BufWriter::new(file); + let session = Session::from(resp); + serde_json::to_writer(writer, &session).map_err(IambError::from)?; + }, + } + + let handle = tokio::spawn(async move { + loop { + let settings = SyncSettings::default(); + + let _ = client.sync(settings).await; + } + }); + + self.sync_handle = Some(handle); + + self.client + .sync_once(SyncSettings::default()) + .await + .map_err(IambError::from)?; + + Ok(Some(InfoMessage::from("Successfully logged in!"))) + } + + async fn send_message(&mut self, room_id: OwnedRoomId, msg: String) -> IambResult { + let room = if let r @ Some(_) = self.client.get_joined_room(&room_id) { + r + } else if self.client.join_room_by_id(&room_id).await.is_ok() { + self.client.get_joined_room(&room_id) + } else { + None + }; + + if let Some(room) = room { + let msg = TextMessageEventContent::plain(msg); + let msg = MessageType::Text(msg); + let msg = RoomMessageEventContent::new(msg); + + // XXX: second parameter can be a locally unique transaction id. + // Useful for doing retries. + let resp = room.send(msg.clone(), None).await.map_err(IambError::from)?; + let event_id = resp.event_id; + + // XXX: need to either give error messages and retry when needed! + + return Ok((event_id, msg)); + } else { + Err(IambError::UnknownRoom(room_id).into()) + } + } + + async fn direct_message(&mut self, user: OwnedUserId) -> IambResult<(MatrixRoom, DisplayName)> { + for (room, name) in self.direct_messages().await { + if room.get_member(user.as_ref()).await.map_err(IambError::from)?.is_some() { + return Ok((room, name)); + } + } + + let mut request = CreateRoomRequest::new(); + let invite = [user.clone()]; + request.is_direct = true; + request.invite = &invite; + request.visibility = Visibility::Private; + request.preset = Some(RoomPreset::PrivateChat); + + match self.client.create_room(request).await { + Ok(resp) => self.get_room(resp.room_id).await, + Err(e) => { + error!( + user_id = user.as_str(), + err = e.to_string(), + "Failed to create direct message room" + ); + + let msg = format!("Could not open a room with {}", user); + let err = UIError::Failure(msg); + + Err(err) + }, + } + } + + async fn get_room(&mut self, room_id: OwnedRoomId) -> IambResult<(MatrixRoom, DisplayName)> { + if let Some(room) = self.client.get_room(&room_id) { + let name = room.display_name().await.map_err(IambError::from)?; + + Ok((room, name)) + } else { + Err(IambError::UnknownRoom(room_id).into()) + } + } + + async fn join_room(&mut self, name: String) -> IambResult { + if let Ok(alias_id) = OwnedRoomOrAliasId::from_str(name.as_str()) { + match self.client.join_room_by_id_or_alias(&alias_id, &[]).await { + Ok(resp) => Ok(resp.room_id), + Err(e) => { + let msg = e.to_string(); + let err = UIError::Failure(msg); + + return Err(err); + }, + } + } else if let Ok(user) = OwnedUserId::try_from(name.as_str()) { + let room = self.direct_message(user).await?.0; + + return Ok(room.room_id().to_owned()); + } else { + let msg = format!("{:?} is not a valid room or user name", name.as_str()); + let err = UIError::Failure(msg); + + return Err(err); + } + } + + async fn direct_messages(&mut self) -> Vec<(MatrixRoom, DisplayName)> { + let mut rooms = vec![]; + + for room in self.client.joined_rooms().into_iter() { + if room.is_space() || !room.is_direct() { + continue; + } + + if let Ok(name) = room.display_name().await { + rooms.push((MatrixRoom::from(room), name)) + } + } + + return rooms; + } + + async fn joined_rooms(&mut self) -> Vec<(MatrixRoom, DisplayName)> { + let mut rooms = vec![]; + + for room in self.client.joined_rooms().into_iter() { + if room.is_space() || room.is_direct() { + continue; + } + + if let Ok(name) = room.display_name().await { + rooms.push((MatrixRoom::from(room), name)) + } + } + + return rooms; + } + + async fn load_older( + &mut self, + room_id: OwnedRoomId, + fetch_id: Option, + limit: u32, + ) -> MessageFetchResult { + if let Some(room) = self.client.get_room(room_id.as_ref()) { + let mut opts = match &fetch_id { + Some(id) => MessagesOptions::backward().from(id.as_str()), + None => MessagesOptions::backward(), + }; + opts.limit = limit.into(); + + let Messages { end, chunk, .. } = room.messages(opts).await.map_err(IambError::from)?; + + let msgs = chunk.into_iter().filter_map(|ev| { + match ev.event.deserialize() { + Ok(AnyTimelineEvent::MessageLike(msg)) => { + if let AnyMessageLikeEvent::RoomMessage(msg) = msg { + Some(msg) + } else { + None + } + }, + Ok(AnyTimelineEvent::State(_)) => None, + Err(_) => None, + } + }); + + Ok((end, msgs.collect())) + } else { + Err(IambError::UnknownRoom(room_id).into()) + } + } + + async fn space_members(&mut self, space: OwnedRoomId) -> IambResult> { + let mut req = SpaceHierarchyRequest::new(&space); + req.limit = Some(1000u32.into()); + req.max_depth = Some(1u32.into()); + + let resp = self.client.send(req, None).await.map_err(IambError::from)?; + + let rooms = resp.rooms.into_iter().map(|chunk| chunk.room_id).collect(); + + Ok(rooms) + } + + async fn spaces(&mut self) -> Vec<(MatrixRoom, DisplayName)> { + let mut spaces = vec![]; + + for room in self.client.joined_rooms().into_iter() { + if !room.is_space() { + continue; + } + + if let Ok(name) = room.display_name().await { + spaces.push((MatrixRoom::from(room), name)); + } + } + + return spaces; + } + + async fn verify(&self, action: VerifyAction, sas: SasVerification) -> IambResult { + match action { + VerifyAction::Accept => { + sas.accept().await.map_err(IambError::from)?; + + Ok(Some(InfoMessage::from("Accepted verification request"))) + }, + VerifyAction::Confirm => { + if sas.is_done() || sas.is_cancelled() { + let msg = "Can only confirm in-progress verifications!"; + let err = UIError::Failure(msg.into()); + + return Err(err); + } + + sas.confirm().await.map_err(IambError::from)?; + + Ok(Some(InfoMessage::from("Confirmed verification"))) + }, + VerifyAction::Cancel => { + if sas.is_done() || sas.is_cancelled() { + let msg = "Can only cancel in-progress verifications!"; + let err = UIError::Failure(msg.into()); + + return Err(err); + } + + sas.cancel().await.map_err(IambError::from)?; + + Ok(Some(InfoMessage::from("Cancelled verification"))) + }, + VerifyAction::Mismatch => { + if sas.is_done() || sas.is_cancelled() { + let msg = "Can only cancel in-progress verifications!"; + let err = UIError::Failure(msg.into()); + + return Err(err); + } + + sas.mismatch().await.map_err(IambError::from)?; + + Ok(Some(InfoMessage::from("Cancelled verification"))) + }, + } + } + + async fn verify_request(&self, user_id: OwnedUserId) -> IambResult { + let enc = self.client.encryption(); + + match enc.get_user_identity(user_id.as_ref()).await.map_err(IambError::from)? { + Some(identity) => { + let methods = vec![VerificationMethod::SasV1]; + let request = identity.request_verification_with_methods(methods); + let _req = request.await.map_err(IambError::from)?; + let info = format!("Sent verification request to {}", user_id); + + Ok(InfoMessage::from(info).into()) + }, + None => { + let msg = format!("Could not find identity information for {}", user_id); + let err = UIError::Failure(msg); + + Err(err) + }, + } + } +}