feat(labs): Remove `jack-in`.
This was a fun ride, but nobody uses it, and we don't need it anymore. Thanks for all the fishes.
This commit is contained in:
parent
7dd086fcdc
commit
ea5b4c98c1
|
@ -109,24 +109,6 @@ version = "0.13.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
|
||||
|
||||
[[package]]
|
||||
name = "app_dirs2"
|
||||
version = "2.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7e7b35733e3a8c1ccb90385088dd5b6eaa61325cb4d1ad56e683b5224ff352e"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"ndk-context",
|
||||
"winapi",
|
||||
"xdg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.6"
|
||||
|
@ -661,12 +643,6 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
|
@ -688,12 +664,6 @@ version = "1.0.79"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
|
@ -806,7 +776,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap 0.16.0",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -893,16 +863,6 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.1.0"
|
||||
|
@ -912,19 +872,6 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"unicode-width",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
|
@ -1100,31 +1047,6 @@ 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"
|
||||
|
@ -1295,29 +1217,6 @@ dependencies = [
|
|||
"const-oid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af3c796f3b0b408d9fd581611b47fa850821fcb84aa640b83a3c1a5be2d691f2"
|
||||
dependencies = [
|
||||
"console",
|
||||
"shell-words",
|
||||
"tempfile",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.9.0"
|
||||
|
@ -1400,12 +1299,6 @@ version = "1.8.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.32"
|
||||
|
@ -2397,55 +2290,6 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
|
||||
|
||||
[[package]]
|
||||
name = "jack-in"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"app_dirs2",
|
||||
"chrono",
|
||||
"clap 4.1.8",
|
||||
"dialoguer",
|
||||
"eyeball",
|
||||
"eyeball-im",
|
||||
"eyre",
|
||||
"futures",
|
||||
"log4rs",
|
||||
"matrix-sdk",
|
||||
"matrix-sdk-common",
|
||||
"matrix-sdk-sled",
|
||||
"sanitize-filename-reader-friendly",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-flame",
|
||||
"tracing-subscriber",
|
||||
"tui-logger",
|
||||
"tui-realm-stdlib",
|
||||
"tuirealm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19bfb8e36ca99b00e6d368320e0822dec9d81db4ccf122f82091f972c90b9985"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.1.22"
|
||||
|
@ -2522,29 +2366,6 @@ dependencies = [
|
|||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-regex"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a505da2f89befd87ab425d252795f0f285e100b43e7d22d29528df3d9a576793"
|
||||
dependencies = [
|
||||
"lazy-regex-proc_macros",
|
||||
"once_cell",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-regex-proc_macros"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8edfc11b8f56ce85e207e62ea21557cfa09bb24a8f6b04ae181b086ff8611c22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
@ -2619,12 +2440,6 @@ dependencies = [
|
|||
"value-bag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log-mdc"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7"
|
||||
|
||||
[[package]]
|
||||
name = "log-panics"
|
||||
version = "2.1.0"
|
||||
|
@ -2635,24 +2450,6 @@ dependencies = [
|
|||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log4rs"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d36ca1786d9e79b8193a68d480a0907b612f109537115c6ff655a3a1967533fd"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"chrono",
|
||||
"derivative",
|
||||
"fnv",
|
||||
"log",
|
||||
"log-mdc",
|
||||
"parking_lot 0.12.1",
|
||||
"thiserror",
|
||||
"thread-id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
version = "0.1.1"
|
||||
|
@ -3326,12 +3123,6 @@ dependencies = [
|
|||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.4"
|
||||
|
@ -4879,12 +4670,6 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.15"
|
||||
|
@ -4895,17 +4680,6 @@ dependencies = [
|
|||
"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.1"
|
||||
|
@ -4967,24 +4741,12 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slog"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.4.7"
|
||||
|
@ -5152,17 +4914,6 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
|
@ -5189,17 +4940,6 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.7"
|
||||
|
@ -5594,17 +5334,6 @@ dependencies = [
|
|||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-flame"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bae117ee14789185e129aaee5d93750abe67fdc5a9a62650452bfe4e122a3a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-futures"
|
||||
version = "0.2.5"
|
||||
|
@ -5665,70 +5394,6 @@ version = "0.2.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||
|
||||
[[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 = "tui-logger"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a10e7f291d91032ade13ba20e71d4c4c26daa3fa3edf58b9dd5aa7595240f7"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"fxhash",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot 0.12.1",
|
||||
"slog",
|
||||
"tui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tui-realm-stdlib"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66f252bf8b07c6fd708ddd6349b5f044ae5b488b26929c745728d9c7e2cebfa6"
|
||||
dependencies = [
|
||||
"textwrap 0.15.2",
|
||||
"tuirealm",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tuirealm"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "265411b5606f400459af94fbc5aae6a7bc0e98094d08cb5868390c932be88e26"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm",
|
||||
"lazy-regex",
|
||||
"thiserror",
|
||||
"tui",
|
||||
"tuirealm_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tuirealm_derive"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0adcdaf59881626555558eae08f8a53003c8a1961723b4d7a10c51599abbc81"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.16.0"
|
||||
|
@ -5762,16 +5427,6 @@ version = "1.0.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
|
@ -6394,15 +6049,6 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c4583db5cbd4c4c0303df2d15af80f0539db703fa1c68802d4cbbd2dd0f88f6"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xshell"
|
||||
version = "0.1.17"
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
[package]
|
||||
name = "jack-in"
|
||||
publish = false
|
||||
description = "an experimental sliding sync/syncv3 terminal client to jack into the matrix"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
file-logging = ["dep:log4rs"]
|
||||
|
||||
[dependencies]
|
||||
app_dirs2 = "2"
|
||||
chrono = "0.4.23"
|
||||
clap = { version = "4.0.29", features = ["derive", "env"] }
|
||||
dialoguer = "0.10.2"
|
||||
eyeball = { workspace = true }
|
||||
eyeball-im = { workspace = true }
|
||||
eyre = "0.6"
|
||||
futures = { version = "0.3.1" }
|
||||
matrix-sdk = { path = "../../crates/matrix-sdk", default-features = false, features = ["e2e-encryption", "anyhow", "native-tls", "sled", "experimental-sliding-sync", "experimental-timeline"], version = "0.6.0" }
|
||||
matrix-sdk-common = { path = "../../crates/matrix-sdk-common", version = "0.6.0" }
|
||||
matrix-sdk-sled = { path = "../../crates/matrix-sdk-sled", features = ["state-store", "crypto-store"], version = "0.2.0" }
|
||||
sanitize-filename-reader-friendly = "2.2.1"
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
tracing-flame = "0.2"
|
||||
tracing-subscriber = "0.3.15"
|
||||
tui-logger = "0.8.1"
|
||||
tuirealm = "~1.8"
|
||||
tui-realm-stdlib = "1.2.0"
|
||||
|
||||
# file-logging specials
|
||||
tracing = { version = "0.1.35", features = ["log"] }
|
||||
log4rs = { version = "1.1.1", default-features = false, features = ["file_appender"], optional = true }
|
|
@ -1,47 +0,0 @@
|
|||
# Jack-in
|
||||
_your experimental terminal to connect to matrix via sliding sync_
|
||||
|
||||
A simple example client, using sliding sync to [jack in](https://matrix.fandom.com/wiki/Jacking_in) to matrix via the sliding-sync-proxy.
|
||||
|
||||
## Use:
|
||||
|
||||
You will need a [running sliding sync proxy](https://github.com/matrix-org/sliding-sync/) for now.
|
||||
|
||||
From the roof of this workspace, run jack-in via `cargo run -p jack-in` . Please note that you need to specify the access-token and username, both can be done via environment variables, too. As well as the homeserver (or `http://localhost:8008` will be assumed). See below for how to acquire an access token.
|
||||
|
||||
```
|
||||
Your experimental sliding-sync jack into the matrix
|
||||
|
||||
Usage: jack-in [OPTIONS] --user <USER>
|
||||
|
||||
Options:
|
||||
-p, --password <PASSWORD>
|
||||
The password of your account. If not given and no database found, it will prompt you for it [env: JACKIN_PASSWORD=]
|
||||
--fresh
|
||||
Create a fresh database, drop all existing cache
|
||||
-l, --log <LOG>
|
||||
RUST_LOG log-levels [env: JACKIN_LOG=] [default: jack_in=info,warn]
|
||||
-u, --user <USER>
|
||||
The userID to log in with [env: JACKIN_USER=]
|
||||
--store-pass <STORE_PASS>
|
||||
The password to encrypt the store with [env: JACKIN_STORE_PASSWORD=]
|
||||
--flames <FLAMES>
|
||||
Activate tracing and write the flamegraph to the specified file
|
||||
--sliding-sync-proxy <PROXY>
|
||||
The address of the sliding sync server to connect (probs the proxy) [env: JACKIN_SYNC_PROXY=] [default: http://localhost:8008]
|
||||
--full-sync-mode <FULL_SYNC_MODE>
|
||||
Activate growing window rather than pagination for full-sync [default: paging] [possible values: growing, paging]
|
||||
--limit <LIMIT>
|
||||
Limit the growing/paging to this number of maximum items to caonsider "done"
|
||||
--batch-size <BATCH_SIZE>
|
||||
define the batch_size per request
|
||||
--timeline-limit <TIMELINE_LIMIT>
|
||||
define the timeline items to load
|
||||
-h, --help
|
||||
Print help information
|
||||
```
|
||||
|
||||
|
||||
### Get the access token
|
||||
1. In [element.io](https://develop.element.org) navigate to `Settings` -> `Help & About`, under _Advanced_ (on the bottom) you can find your Access token
|
||||
2. Copy it and set as the `JACKIN_SYNC_TOKEN` environment variable or as `--token` cli-parameter on jack-in run
|
|
@ -1,2 +0,0 @@
|
|||
pub use super::*;
|
||||
pub mod model;
|
|
@ -1,230 +0,0 @@
|
|||
//! ## Model
|
||||
//!
|
||||
//! app model
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use futures::executor::block_on;
|
||||
use matrix_sdk::{ruma::events::room::message::RoomMessageEventContent, Client};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::warn;
|
||||
use tuirealm::{
|
||||
props::{Alignment, Borders, Color},
|
||||
terminal::TerminalBridge,
|
||||
tui::layout::{Constraint, Direction, Layout},
|
||||
Application, AttrValue, Attribute, EventListenerCfg, Sub, SubClause, SubEventClause, Update,
|
||||
};
|
||||
|
||||
use super::{
|
||||
components::{Details, InputText, Label, Logger, Rooms, StatusBar},
|
||||
Id, JackInEvent, MatrixPoller, Msg,
|
||||
};
|
||||
use crate::client::state::SlidingSyncState;
|
||||
|
||||
pub struct Model {
|
||||
/// Application
|
||||
pub app: Application<Id, Msg, JackInEvent>,
|
||||
/// Indicates that the application must quit
|
||||
pub quit: bool,
|
||||
/// Tells whether to redraw interface
|
||||
pub redraw: bool,
|
||||
/// Used to draw to terminal
|
||||
pub terminal: TerminalBridge,
|
||||
/// show the logger console
|
||||
pub show_logger: bool,
|
||||
sliding_sync: SlidingSyncState,
|
||||
tx: mpsc::Sender<SlidingSyncState>,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn new(
|
||||
sliding_sync: SlidingSyncState,
|
||||
tx: mpsc::Sender<SlidingSyncState>,
|
||||
poller: MatrixPoller,
|
||||
client: Client,
|
||||
) -> Self {
|
||||
let app = Self::init_app(sliding_sync.clone(), poller);
|
||||
|
||||
Self {
|
||||
app,
|
||||
tx,
|
||||
sliding_sync,
|
||||
quit: false,
|
||||
redraw: true,
|
||||
terminal: TerminalBridge::new().expect("Cannot initialize terminal"),
|
||||
show_logger: true,
|
||||
client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn set_title(&mut self, title: String) {
|
||||
assert!(self.app.attr(&Id::Label, Attribute::Text, AttrValue::String(title),).is_ok());
|
||||
}
|
||||
pub fn view(&mut self) {
|
||||
assert!(self
|
||||
.terminal
|
||||
.raw_mut()
|
||||
.draw(|f| {
|
||||
let mut areas = vec![
|
||||
Constraint::Length(3), // Header
|
||||
Constraint::Min(10), // body
|
||||
Constraint::Length(3), // Status Footer
|
||||
];
|
||||
if self.show_logger {
|
||||
areas.push(
|
||||
Constraint::Length(12), // logs
|
||||
);
|
||||
}
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(areas)
|
||||
.split(f.size());
|
||||
|
||||
// Body
|
||||
let body_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Length(35), Constraint::Min(23)].as_ref())
|
||||
.split(chunks[1]);
|
||||
|
||||
let details_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Min(10), Constraint::Max(3)].as_ref())
|
||||
.split(body_chunks[1]);
|
||||
|
||||
self.app.view(&Id::Rooms, f, body_chunks[0]);
|
||||
self.app.view(&Id::Details, f, details_chunks[0]);
|
||||
self.app.view(&Id::TextMessage, f, details_chunks[1]);
|
||||
self.app.view(&Id::Status, f, chunks[2]);
|
||||
self.app.view(&Id::Label, f, chunks[0]);
|
||||
if self.show_logger {
|
||||
self.app.view(&Id::Logger, f, chunks[3]);
|
||||
}
|
||||
})
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
fn init_app(
|
||||
sliding_sync: SlidingSyncState,
|
||||
poller: MatrixPoller,
|
||||
) -> Application<Id, Msg, JackInEvent> {
|
||||
let mut app: Application<Id, Msg, JackInEvent> = Application::init(
|
||||
EventListenerCfg::default()
|
||||
.default_input_listener(Duration::from_millis(20))
|
||||
.port(Box::new(poller), Duration::from_millis(100))
|
||||
.poll_timeout(Duration::from_millis(10))
|
||||
.tick_interval(Duration::from_millis(200)),
|
||||
);
|
||||
// Mount components
|
||||
assert!(app
|
||||
.mount(
|
||||
Id::Label,
|
||||
Box::new(
|
||||
Label::default()
|
||||
.text("Loading")
|
||||
.alignment(Alignment::Center)
|
||||
.background(Color::Reset)
|
||||
.borders(Borders::default())
|
||||
.foreground(Color::Green),
|
||||
),
|
||||
Vec::default(),
|
||||
)
|
||||
.is_ok());
|
||||
// mount logger
|
||||
assert!(app.remount(Id::Logger, Box::<Logger>::default(), Vec::default()).is_ok());
|
||||
|
||||
assert!(app
|
||||
.mount(
|
||||
Id::Status,
|
||||
Box::new(StatusBar::new(sliding_sync.clone())),
|
||||
vec![Sub::new(SubEventClause::Any, SubClause::Always)]
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
assert!(app.mount(Id::TextMessage, Box::<InputText>::default(), vec![]).is_ok());
|
||||
assert!(app
|
||||
.mount(
|
||||
Id::Rooms,
|
||||
Box::new(
|
||||
Rooms::new(sliding_sync.clone())
|
||||
.borders(Borders::default().color(Color::Green))
|
||||
),
|
||||
vec![Sub::new(SubEventClause::Any, SubClause::Always)]
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
assert!(app
|
||||
.mount(
|
||||
Id::Details,
|
||||
Box::new(
|
||||
Details::new(sliding_sync).borders(Borders::default().color(Color::Green))
|
||||
),
|
||||
vec![Sub::new(SubEventClause::Any, SubClause::Always)]
|
||||
)
|
||||
.is_ok());
|
||||
// Active letter counter
|
||||
assert!(app.active(&Id::Rooms).is_ok());
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
// Let's implement Update for model
|
||||
|
||||
impl Update<Msg> for Model {
|
||||
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
|
||||
if let Some(msg) = msg {
|
||||
// Set redraw
|
||||
self.redraw = true;
|
||||
// Match message
|
||||
match msg {
|
||||
Msg::AppClose => {
|
||||
self.quit = true; // Terminate
|
||||
None
|
||||
}
|
||||
Msg::Clock => None,
|
||||
Msg::RoomsBlur => {
|
||||
// Give focus to room details
|
||||
if self.sliding_sync.has_selected_room() {
|
||||
let _ = self.app.blur();
|
||||
assert!(self.app.active(&Id::Details).is_ok());
|
||||
}
|
||||
None
|
||||
}
|
||||
Msg::DetailsBlur => {
|
||||
// Give focus to room list
|
||||
let _ = self.app.blur();
|
||||
assert!(self.app.active(&Id::TextMessage).is_ok());
|
||||
None
|
||||
}
|
||||
Msg::TextBlur => {
|
||||
// Give focus to room list
|
||||
let _ = self.app.blur();
|
||||
assert!(self.app.active(&Id::Rooms).is_ok());
|
||||
None
|
||||
}
|
||||
Msg::SelectRoom(r) => {
|
||||
warn!("setting room, sending msg");
|
||||
self.sliding_sync.select_room(r);
|
||||
let _ = self.tx.try_send(self.sliding_sync.clone());
|
||||
None
|
||||
}
|
||||
Msg::SendMessage(m) => {
|
||||
if let Some(tl) = &*self.sliding_sync.room_timeline.read() {
|
||||
block_on(async move {
|
||||
// fire and forget
|
||||
tl.send(RoomMessageEventContent::text_plain(m).into(), None).await;
|
||||
});
|
||||
} else {
|
||||
warn!("asked to send message, but no room is selected");
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
use eyre::{Result, WrapErr};
|
||||
use futures::{pin_mut, StreamExt};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
pub mod state;
|
||||
|
||||
use matrix_sdk::{
|
||||
ruma::{api::client::error::ErrorKind, OwnedRoomId},
|
||||
Client, SlidingSyncListBuilder, SlidingSyncState,
|
||||
};
|
||||
|
||||
pub async fn run_client(
|
||||
client: Client,
|
||||
tx: mpsc::Sender<state::SlidingSyncState>,
|
||||
config: crate::SlidingSyncConfig,
|
||||
) -> Result<()> {
|
||||
info!("Starting sliding sync now");
|
||||
let builder = client.sliding_sync().await;
|
||||
let mut full_sync_view_builder = SlidingSyncListBuilder::default_with_fullsync()
|
||||
.timeline_limit(10u32)
|
||||
.sync_mode(config.full_sync_mode.into());
|
||||
if let Some(size) = config.batch_size {
|
||||
full_sync_view_builder = full_sync_view_builder.full_sync_batch_size(size);
|
||||
}
|
||||
|
||||
if let Some(limit) = config.limit {
|
||||
full_sync_view_builder =
|
||||
full_sync_view_builder.full_sync_maximum_number_of_rooms_to_fetch(limit);
|
||||
}
|
||||
if let Some(limit) = config.timeline_limit {
|
||||
full_sync_view_builder = full_sync_view_builder.timeline_limit(limit);
|
||||
}
|
||||
|
||||
let full_sync_view = full_sync_view_builder.build()?;
|
||||
|
||||
let syncer = builder
|
||||
.homeserver(config.proxy.parse().wrap_err("can't parse sync proxy")?)
|
||||
.add_list(full_sync_view)
|
||||
.with_common_extensions()
|
||||
.cold_cache("jack-in-default")
|
||||
.build()
|
||||
.await?;
|
||||
let stream = syncer.stream();
|
||||
let view = syncer.list("full-sync").expect("we have the full syncer there").clone();
|
||||
let mut ssync_state = state::SlidingSyncState::new(syncer.clone(), view.clone());
|
||||
tx.send(ssync_state.clone()).await?;
|
||||
|
||||
info!("starting polling");
|
||||
|
||||
pin_mut!(stream);
|
||||
if let Some(Err(e)) = stream.next().await {
|
||||
error!("Stopped: Initial Query on sliding sync failed: {e:?}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
{
|
||||
ssync_state.set_first_render_now();
|
||||
tx.send(ssync_state.clone()).await?;
|
||||
}
|
||||
info!("Done initial sliding sync");
|
||||
|
||||
loop {
|
||||
match stream.next().await {
|
||||
Some(Ok(_)) => {
|
||||
// we are switching into live updates mode next. ignoring
|
||||
let state = view.state();
|
||||
ssync_state.set_view_state(state.clone());
|
||||
|
||||
if state == SlidingSyncState::FullyLoaded {
|
||||
info!("Reached live sync");
|
||||
break;
|
||||
}
|
||||
let _ = tx.send(ssync_state.clone()).await;
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
if e.client_api_error_kind() != Some(&ErrorKind::UnknownPos) {
|
||||
error!("Error: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("Never reached live state");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ssync_state.set_full_sync_now();
|
||||
tx.send(ssync_state.clone()).await?;
|
||||
}
|
||||
|
||||
let mut err_counter = 0;
|
||||
let mut prev_selected_room: Option<OwnedRoomId> = None;
|
||||
|
||||
while let Some(update) = stream.next().await {
|
||||
{
|
||||
let selected_room = ssync_state.selected_room.get();
|
||||
if let Some(room_id) = selected_room {
|
||||
if let Some(prev) = &prev_selected_room {
|
||||
if prev != &room_id {
|
||||
syncer.unsubscribe(prev.clone());
|
||||
syncer.subscribe(room_id.clone(), None);
|
||||
prev_selected_room = Some(room_id.clone());
|
||||
}
|
||||
} else {
|
||||
syncer.subscribe(room_id.clone(), None);
|
||||
prev_selected_room = Some(room_id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
match update {
|
||||
Ok(update) => {
|
||||
info!("Live update received: {update:?}");
|
||||
tx.send(ssync_state.clone()).await?;
|
||||
err_counter = 0;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Live update error: {e:?}");
|
||||
err_counter += 1;
|
||||
if err_counter > 3 {
|
||||
error!("Received 3 errors in a row. stopping.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
use std::{
|
||||
sync::{Arc, RwLock as StdRwLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use eyeball::shared::Observable as SharedObservable;
|
||||
use eyeball_im::{ObservableVector, VectorDiff};
|
||||
use futures::{pin_mut, StreamExt};
|
||||
use matrix_sdk::{
|
||||
room::timeline::{Timeline, TimelineItem},
|
||||
ruma::{OwnedRoomId, RoomId},
|
||||
SlidingSync, SlidingSyncList, SlidingSyncRoom, SlidingSyncState as ViewState,
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CurrentRoomSummary {
|
||||
pub name: String,
|
||||
pub state_events_counts: Vec<(String, usize)>,
|
||||
//pub state_events: BTreeMap<String, Vec<SlidingSyncRoom>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SlidingSyncState {
|
||||
started: Instant,
|
||||
syncer: SlidingSync,
|
||||
view: SlidingSyncList,
|
||||
/// the current list selector for the room
|
||||
first_render: Option<Duration>,
|
||||
full_sync: Option<Duration>,
|
||||
current_state: ViewState,
|
||||
tl_handle: SharedObservable<Option<JoinHandle<()>>>,
|
||||
pub selected_room: SharedObservable<Option<OwnedRoomId>>,
|
||||
pub current_timeline: Arc<StdRwLock<ObservableVector<Arc<TimelineItem>>>>,
|
||||
pub room_timeline: SharedObservable<Option<Timeline>>,
|
||||
}
|
||||
|
||||
impl SlidingSyncState {
|
||||
pub fn new(syncer: SlidingSync, view: SlidingSyncList) -> Self {
|
||||
Self {
|
||||
started: Instant::now(),
|
||||
syncer,
|
||||
view,
|
||||
first_render: None,
|
||||
full_sync: None,
|
||||
current_state: ViewState::default(),
|
||||
tl_handle: Default::default(),
|
||||
selected_room: Default::default(),
|
||||
current_timeline: Default::default(),
|
||||
room_timeline: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn started(&self) -> &Instant {
|
||||
&self.started
|
||||
}
|
||||
|
||||
pub fn has_selected_room(&self) -> bool {
|
||||
self.selected_room.read().is_some()
|
||||
}
|
||||
|
||||
pub fn select_room(&self, r: Option<OwnedRoomId>) {
|
||||
self.current_timeline.write().unwrap().clear();
|
||||
if let Some(c) = self.tl_handle.take() {
|
||||
c.abort();
|
||||
}
|
||||
if let Some(room) = r.as_ref().and_then(|room_id| self.get_room(room_id)) {
|
||||
let current_timeline = self.current_timeline.clone();
|
||||
let room_timeline = self.room_timeline.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let timeline = room.timeline().await.unwrap();
|
||||
let (items, listener) = timeline.subscribe().await;
|
||||
room_timeline.set(Some(timeline));
|
||||
{
|
||||
let mut lock = current_timeline.write().unwrap();
|
||||
lock.clear();
|
||||
lock.append(items.into_iter().collect());
|
||||
}
|
||||
pin_mut!(listener);
|
||||
while let Some(diff) = listener.next().await {
|
||||
match diff {
|
||||
VectorDiff::Append { values } => {
|
||||
current_timeline.write().unwrap().append(values);
|
||||
}
|
||||
VectorDiff::Clear => {
|
||||
current_timeline.write().unwrap().clear();
|
||||
}
|
||||
VectorDiff::Insert { index, value } => {
|
||||
current_timeline.write().unwrap().insert(index, value);
|
||||
}
|
||||
VectorDiff::PopBack => {
|
||||
current_timeline.write().unwrap().pop_back();
|
||||
}
|
||||
VectorDiff::PopFront => {
|
||||
current_timeline.write().unwrap().pop_front();
|
||||
}
|
||||
VectorDiff::PushBack { value } => {
|
||||
current_timeline.write().unwrap().push_back(value);
|
||||
}
|
||||
VectorDiff::PushFront { value } => {
|
||||
current_timeline.write().unwrap().push_front(value);
|
||||
}
|
||||
VectorDiff::Remove { index } => {
|
||||
current_timeline.write().unwrap().remove(index);
|
||||
}
|
||||
VectorDiff::Set { index, value } => {
|
||||
current_timeline.write().unwrap().set(index, value);
|
||||
}
|
||||
VectorDiff::Reset { values } => {
|
||||
let mut lock = current_timeline.write().unwrap();
|
||||
lock.clear();
|
||||
lock.append(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.tl_handle.set(Some(handle));
|
||||
}
|
||||
self.selected_room.set(r);
|
||||
}
|
||||
|
||||
pub fn time_to_first_render(&self) -> Option<Duration> {
|
||||
self.first_render
|
||||
}
|
||||
|
||||
pub fn time_to_full_sync(&self) -> Option<Duration> {
|
||||
self.full_sync
|
||||
}
|
||||
pub fn current_state(&self) -> &ViewState {
|
||||
&self.current_state
|
||||
}
|
||||
|
||||
pub fn loaded_rooms_count(&self) -> usize {
|
||||
self.syncer.get_number_of_rooms()
|
||||
}
|
||||
|
||||
pub fn total_rooms_count(&self) -> Option<u32> {
|
||||
self.view.maximum_number_of_rooms()
|
||||
}
|
||||
|
||||
pub fn set_first_render_now(&mut self) {
|
||||
self.first_render = Some(self.started.elapsed())
|
||||
}
|
||||
|
||||
pub fn view(&self) -> &SlidingSyncList {
|
||||
&self.view
|
||||
}
|
||||
|
||||
pub fn get_room(&self, room_id: &RoomId) -> Option<SlidingSyncRoom> {
|
||||
self.syncer.get_room(room_id)
|
||||
}
|
||||
|
||||
pub fn get_all_rooms(&self) -> Vec<SlidingSyncRoom> {
|
||||
self.syncer.get_all_rooms()
|
||||
}
|
||||
|
||||
pub fn set_full_sync_now(&mut self) {
|
||||
self.full_sync = Some(self.started.elapsed())
|
||||
}
|
||||
|
||||
pub fn set_view_state(&mut self, current_state: ViewState) {
|
||||
self.current_state = current_state
|
||||
}
|
||||
}
|
|
@ -1,321 +0,0 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use chrono::{offset::Local, DateTime};
|
||||
use matrix_sdk::room::timeline::TimelineItemContent;
|
||||
use tuirealm::{
|
||||
command::{Cmd, CmdResult},
|
||||
event::{Key, KeyEvent, KeyModifiers},
|
||||
props::{Alignment, Borders, Color, Style},
|
||||
tui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::Modifier,
|
||||
text::Spans,
|
||||
widgets::{Cell, List, ListItem, ListState, Row, Table, Tabs},
|
||||
},
|
||||
AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State,
|
||||
};
|
||||
|
||||
use super::{super::client::state::SlidingSyncState, get_block, JackInEvent, Msg};
|
||||
|
||||
/// ## Details
|
||||
pub struct Details {
|
||||
props: Props,
|
||||
sstate: SlidingSyncState,
|
||||
liststate: ListState,
|
||||
name: Option<String>,
|
||||
state_events_counts: Vec<(String, usize)>,
|
||||
current_room_timeline: Vec<String>,
|
||||
}
|
||||
|
||||
impl Details {
|
||||
pub fn new(sstate: SlidingSyncState) -> Self {
|
||||
Self {
|
||||
props: Props::default(),
|
||||
sstate,
|
||||
liststate: Default::default(),
|
||||
name: None,
|
||||
state_events_counts: Default::default(),
|
||||
current_room_timeline: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_sliding_sync(&mut self, sstate: SlidingSyncState) {
|
||||
self.sstate = sstate;
|
||||
// we gotta refresh data next time it comes around
|
||||
self.name = None;
|
||||
}
|
||||
|
||||
pub fn refresh_data(&mut self) {
|
||||
let Some(room_id) = self.sstate.selected_room.get() else { return };
|
||||
let Some(room_data) = self.sstate.get_room(&room_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let name = room_data.name().unwrap_or("unknown").to_owned();
|
||||
|
||||
let state_events = room_data
|
||||
.required_state()
|
||||
.iter()
|
||||
.filter_map(|r| r.deserialize().ok())
|
||||
.fold(BTreeMap::<String, Vec<_>>::new(), |mut b, r| {
|
||||
let event_name = r.event_type();
|
||||
b.entry(event_name.to_string())
|
||||
.and_modify(|l| l.push(r.clone()))
|
||||
.or_insert_with(|| vec![r.clone()]);
|
||||
b
|
||||
});
|
||||
|
||||
let mut state_events_counts: Vec<(String, usize)> =
|
||||
state_events.iter().map(|(k, l)| (k.clone(), l.len())).collect();
|
||||
state_events_counts.sort_by_key(|(_, count)| *count);
|
||||
|
||||
let timeline: Vec<String> = self
|
||||
.sstate
|
||||
.current_timeline
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter_map(|t| t.as_event()) // we ignore virtual events
|
||||
.map(|e| match e.content() {
|
||||
TimelineItemContent::Message(m) => format!(
|
||||
"[{}] {}: {}",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
m.body()
|
||||
),
|
||||
TimelineItemContent::RedactedMessage => format!(
|
||||
"[{}] {} - redacted -",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
),
|
||||
TimelineItemContent::Sticker(s) => format!(
|
||||
"[{}] {}: {}",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
s.content().body,
|
||||
),
|
||||
TimelineItemContent::MembershipChange(m) => format!(
|
||||
"[{}] {} - membership change '{:?}' for {}",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
m.change(),
|
||||
m.user_id(),
|
||||
),
|
||||
TimelineItemContent::ProfileChange(_) => format!(
|
||||
"[{}] {} - profile change",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
),
|
||||
TimelineItemContent::OtherState(s) => format!(
|
||||
"[{}] {}: '{}' state event",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
s.content().event_type(),
|
||||
),
|
||||
TimelineItemContent::UnableToDecrypt(_) => format!(
|
||||
"[{}] {} - unable to decrypt -",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
),
|
||||
TimelineItemContent::FailedToParseState { event_type, state_key, error } => {
|
||||
format!(
|
||||
"[{}] {} - failed to parse {event_type}({state_key}): {error}",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
)
|
||||
}
|
||||
TimelineItemContent::FailedToParseMessageLike { event_type, error } => format!(
|
||||
"[{}] {} - failed to parse {event_type}: {error}",
|
||||
e.timestamp()
|
||||
.to_system_time()
|
||||
.map(|s| DateTime::<Local>::from(s).format("%Y-%m-%dT%T").to_string())
|
||||
.unwrap_or_default(),
|
||||
e.sender(),
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
self.current_room_timeline = timeline;
|
||||
self.name = Some(name);
|
||||
self.state_events_counts = state_events_counts;
|
||||
}
|
||||
|
||||
pub fn select_dir(&mut self, count: i32) {
|
||||
let total = self.current_room_timeline.len() as i32;
|
||||
let current = self.liststate.selected().unwrap_or_default() as i32;
|
||||
let next = {
|
||||
let next = current + count;
|
||||
if next >= total {
|
||||
next - total
|
||||
} else if next < 0 {
|
||||
total + next
|
||||
} else {
|
||||
next
|
||||
}
|
||||
};
|
||||
self.liststate.select(Some(next.try_into().unwrap_or_default()));
|
||||
}
|
||||
|
||||
pub fn borders(mut self, b: Borders) -> Self {
|
||||
self.attr(Attribute::Borders, AttrValue::Borders(b));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MockComponent for Details {
|
||||
fn view(&mut self, frame: &mut Frame<'_>, area: Rect) {
|
||||
if self.name.is_none() {
|
||||
self.refresh_data();
|
||||
}
|
||||
|
||||
let borders = self
|
||||
.props
|
||||
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
|
||||
.unwrap_borders();
|
||||
|
||||
let Some(name) = &self.name else {
|
||||
// still empty
|
||||
frame.render_widget(
|
||||
Table::new(vec![Row::new(vec![Cell::from(
|
||||
"Choose a room with up/down and press <enter> to select",
|
||||
)])])
|
||||
.block(get_block(
|
||||
borders,
|
||||
("".to_owned(), Alignment::Left),
|
||||
false,
|
||||
)),
|
||||
area,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let areas = vec![
|
||||
Constraint::Length(3), // Events
|
||||
Constraint::Min(10), // Timeline
|
||||
];
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(areas)
|
||||
.split(area);
|
||||
|
||||
let events_title = ("Events".to_owned(), Alignment::Left);
|
||||
|
||||
let events_borders = self
|
||||
.props
|
||||
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
|
||||
.unwrap_borders();
|
||||
|
||||
let mut tabs = vec![];
|
||||
|
||||
for (title, count) in &self.state_events_counts {
|
||||
tabs.push(Spans::from(format!("{title}: {count}")));
|
||||
}
|
||||
|
||||
frame.render_widget(
|
||||
Tabs::new(tabs)
|
||||
.style(Style::default().fg(Color::LightCyan))
|
||||
.block(get_block(events_borders, events_title, false))
|
||||
.style(Style::default().fg(Color::White).bg(Color::Black)),
|
||||
chunks[0],
|
||||
);
|
||||
|
||||
let title = (name.to_owned(), Alignment::Left);
|
||||
let focus = self.props.get_or(Attribute::Focus, AttrValue::Flag(false)).unwrap_flag();
|
||||
|
||||
let details: Vec<_> =
|
||||
self.current_room_timeline.iter().map(|e| ListItem::new(e.clone())).collect();
|
||||
|
||||
frame.render_stateful_widget(
|
||||
List::new(details)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(
|
||||
Style::default().fg(Color::LightCyan).add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.highlight_symbol(">>")
|
||||
.block(get_block(borders, title, focus)),
|
||||
chunks[1],
|
||||
&mut self.liststate,
|
||||
);
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::None
|
||||
}
|
||||
|
||||
fn perform(&mut self, _: Cmd) -> CmdResult {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, JackInEvent> for Details {
|
||||
fn on(&mut self, ev: Event<JackInEvent>) -> Option<Msg> {
|
||||
let focus = self.props.get_or(Attribute::Focus, AttrValue::Flag(false)).unwrap_flag();
|
||||
if focus {
|
||||
// we only care about user input if we are in focus.
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Down, modifiers: KeyModifiers::NONE }) => {
|
||||
self.select_dir(1);
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Down, modifiers: KeyModifiers::SHIFT }) => {
|
||||
self.select_dir(10);
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, modifiers: KeyModifiers::NONE }) => {
|
||||
self.select_dir(-1);
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, modifiers: KeyModifiers::SHIFT }) => {
|
||||
self.select_dir(-10);
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, modifiers: KeyModifiers::NONE }) => {
|
||||
return Some(Msg::DetailsBlur)
|
||||
} // Return focus lost
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, modifiers: KeyModifiers::NONE }) => {
|
||||
return Some(Msg::AppClose)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Event::User(JackInEvent::SyncUpdate(s)) = ev {
|
||||
self.set_sliding_sync(s);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
use tui_realm_stdlib::{states::InputStates, Input};
|
||||
use tuirealm::{
|
||||
command::{Cmd, Direction, Position},
|
||||
event::{Key, KeyEvent, KeyModifiers},
|
||||
props::{Alignment, BorderType, Borders, Color, InputType, Style},
|
||||
Component, Event, MockComponent,
|
||||
};
|
||||
|
||||
use super::{JackInEvent, Msg};
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct InputText {
|
||||
component: Input,
|
||||
}
|
||||
|
||||
impl Default for InputText {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
component: Input::default()
|
||||
.borders(
|
||||
Borders::default().modifiers(BorderType::Rounded).color(Color::LightYellow),
|
||||
)
|
||||
.foreground(Color::LightYellow)
|
||||
.input_type(InputType::Text)
|
||||
.title("Send a message", Alignment::Left)
|
||||
.invalid_style(Style::default().fg(Color::Red)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, JackInEvent> for InputText {
|
||||
fn on(&mut self, ev: Event<JackInEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Left, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Right, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Home, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Delete, .. }) => {
|
||||
self.perform(Cmd::Cancel);
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Backspace, .. }) => {
|
||||
self.perform(Cmd::Delete);
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Char(ch), modifiers: KeyModifiers::NONE }) => {
|
||||
self.perform(Cmd::Type(ch));
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Enter, .. }) => {
|
||||
let input = self.component.states.get_value();
|
||||
self.component.states = InputStates::default();
|
||||
return Some(Msg::SendMessage(input));
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
||||
return Some(Msg::TextBlur);
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
return Some(Msg::AppClose);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
//! ## Label
|
||||
//!
|
||||
//! label component
|
||||
|
||||
use tuirealm::{
|
||||
command::{Cmd, CmdResult},
|
||||
props::{Alignment, Borders, Color, Style, TextModifiers},
|
||||
tui::{
|
||||
layout::Rect,
|
||||
widgets::{Block, Paragraph},
|
||||
},
|
||||
AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State,
|
||||
};
|
||||
|
||||
use super::{JackInEvent, Msg};
|
||||
|
||||
/// ## Label
|
||||
///
|
||||
/// Simple label component; just renders a text
|
||||
/// NOTE: since I need just one label, I'm not going to use different object; I
|
||||
/// will directly implement Component for Label. This is not ideal actually and
|
||||
/// in a real app you should differentiate Mock Components from Application
|
||||
/// Components.
|
||||
#[derive(Default)]
|
||||
pub struct Label {
|
||||
props: Props,
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn text<S>(mut self, s: S) -> Self
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.attr(Attribute::Text, AttrValue::String(s.as_ref().to_owned()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn alignment(mut self, a: Alignment) -> Self {
|
||||
self.attr(Attribute::TextAlign, AttrValue::Alignment(a));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn foreground(mut self, c: Color) -> Self {
|
||||
self.attr(Attribute::Foreground, AttrValue::Color(c));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn background(mut self, c: Color) -> Self {
|
||||
self.attr(Attribute::Background, AttrValue::Color(c));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn borders(mut self, b: Borders) -> Self {
|
||||
self.attr(Attribute::Borders, AttrValue::Borders(b));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MockComponent for Label {
|
||||
fn view(&mut self, frame: &mut Frame<'_>, area: Rect) {
|
||||
// Check if visible
|
||||
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
|
||||
// Get properties
|
||||
let text = self
|
||||
.props
|
||||
.get_or(Attribute::Text, AttrValue::String(String::default()))
|
||||
.unwrap_string();
|
||||
let alignment = self
|
||||
.props
|
||||
.get_or(Attribute::TextAlign, AttrValue::Alignment(Alignment::Left))
|
||||
.unwrap_alignment();
|
||||
let foreground = self
|
||||
.props
|
||||
.get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
|
||||
.unwrap_color();
|
||||
let background = self
|
||||
.props
|
||||
.get_or(Attribute::Background, AttrValue::Color(Color::Reset))
|
||||
.unwrap_color();
|
||||
let modifiers = self
|
||||
.props
|
||||
.get_or(Attribute::TextProps, AttrValue::TextModifiers(TextModifiers::empty()))
|
||||
.unwrap_text_modifiers();
|
||||
|
||||
let borders = self
|
||||
.props
|
||||
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
|
||||
.unwrap_borders();
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(text)
|
||||
.block(Block::default().borders(borders.sides).border_style(borders.style()))
|
||||
.style(Style::default().fg(foreground).bg(background).add_modifier(modifiers))
|
||||
.alignment(alignment),
|
||||
area,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::None
|
||||
}
|
||||
|
||||
fn perform(&mut self, _: Cmd) -> CmdResult {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, JackInEvent> for Label {
|
||||
fn on(&mut self, _: Event<JackInEvent>) -> Option<Msg> {
|
||||
// Does nothing
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
use tui_logger::TuiLoggerWidget;
|
||||
use tuirealm::{
|
||||
command::{Cmd, CmdResult},
|
||||
props::{Alignment, Borders, Color, Style},
|
||||
tui::layout::Rect,
|
||||
AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State,
|
||||
};
|
||||
|
||||
use super::{get_block, JackInEvent, Msg};
|
||||
|
||||
/// ## Logger
|
||||
///
|
||||
/// Simple label component; just renders a text
|
||||
/// NOTE: since I need just one label, I'm not going to use different object; I
|
||||
/// will directly implement Component for Logger. This is not ideal actually and
|
||||
/// in a real app you should differentiate Mock Components from Application
|
||||
/// Components.
|
||||
#[derive(Default)]
|
||||
pub struct Logger {
|
||||
props: Props,
|
||||
}
|
||||
|
||||
impl MockComponent for Logger {
|
||||
fn view(&mut self, frame: &mut Frame<'_>, area: Rect) {
|
||||
let title = ("Logs".to_owned(), Alignment::Center);
|
||||
|
||||
let borders = self
|
||||
.props
|
||||
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
|
||||
.unwrap_borders();
|
||||
let focus = self.props.get_or(Attribute::Focus, AttrValue::Flag(false)).unwrap_flag();
|
||||
frame.render_widget(
|
||||
TuiLoggerWidget::default()
|
||||
.style_error(Style::default().fg(Color::Red))
|
||||
.style_debug(Style::default().fg(Color::Green))
|
||||
.style_warn(Style::default().fg(Color::Yellow))
|
||||
.style_trace(Style::default().fg(Color::Gray))
|
||||
.style_info(Style::default().fg(Color::Blue))
|
||||
.block(get_block(borders, title, focus))
|
||||
.style(Style::default().fg(Color::White).bg(Color::Black)),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::None
|
||||
}
|
||||
|
||||
fn perform(&mut self, _: Cmd) -> CmdResult {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, JackInEvent> for Logger {
|
||||
fn on(&mut self, _: Event<JackInEvent>) -> Option<Msg> {
|
||||
// Does nothing
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
//! ## Components
|
||||
//!
|
||||
//! demo example components
|
||||
|
||||
use tuirealm::{
|
||||
props::{Alignment, Borders, Color, Style},
|
||||
tui::widgets::Block,
|
||||
};
|
||||
|
||||
use super::{JackInEvent, Msg};
|
||||
|
||||
// -- modules
|
||||
mod details;
|
||||
mod input;
|
||||
mod label;
|
||||
mod logger;
|
||||
mod rooms;
|
||||
mod statusbar;
|
||||
|
||||
// -- export
|
||||
pub use details::Details;
|
||||
pub use input::InputText;
|
||||
pub use label::Label;
|
||||
pub use logger::Logger;
|
||||
pub use rooms::Rooms;
|
||||
pub use statusbar::StatusBar;
|
||||
|
||||
/// ### get_block
|
||||
///
|
||||
/// Get block
|
||||
pub(crate) fn get_block<'a>(props: Borders, title: (String, Alignment), focus: bool) -> Block<'a> {
|
||||
Block::default()
|
||||
.borders(props.sides)
|
||||
.border_style(match focus {
|
||||
true => props.style(),
|
||||
false => Style::default().fg(Color::Reset).bg(Color::Reset),
|
||||
})
|
||||
.border_type(props.modifiers)
|
||||
.title(title.0)
|
||||
.title_alignment(title.1)
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
use tracing::warn;
|
||||
use tuirealm::{
|
||||
command::{Cmd, CmdResult},
|
||||
event::{Key, KeyEvent, KeyModifiers},
|
||||
props::{Alignment, Borders, Color, Style},
|
||||
tui::{
|
||||
layout::{Constraint, Rect},
|
||||
style::Modifier,
|
||||
widgets::{Cell, Row, Table, TableState},
|
||||
},
|
||||
AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State,
|
||||
};
|
||||
|
||||
use super::{super::client::state::SlidingSyncState, get_block, JackInEvent, Msg};
|
||||
|
||||
/// ## Rooms
|
||||
pub struct Rooms {
|
||||
props: Props,
|
||||
sstate: SlidingSyncState,
|
||||
tablestate: TableState,
|
||||
}
|
||||
|
||||
impl Rooms {
|
||||
pub fn new(sstate: SlidingSyncState) -> Self {
|
||||
Self { props: Props::default(), sstate, tablestate: Default::default() }
|
||||
}
|
||||
|
||||
pub fn set_sliding_sync(&mut self, sstate: SlidingSyncState) {
|
||||
self.sstate = sstate;
|
||||
}
|
||||
|
||||
pub fn select_dir(&mut self, count: i32) {
|
||||
let rooms_count = self.sstate.loaded_rooms_count() as i32;
|
||||
let current = self.tablestate.selected().unwrap_or_default() as i32;
|
||||
let next = {
|
||||
let next = current + count;
|
||||
if next >= rooms_count {
|
||||
next - rooms_count
|
||||
} else if next < 0 {
|
||||
rooms_count + next
|
||||
} else {
|
||||
next
|
||||
}
|
||||
};
|
||||
self.tablestate.select(Some(next.try_into().unwrap_or_default()));
|
||||
}
|
||||
|
||||
pub fn borders(mut self, b: Borders) -> Self {
|
||||
self.attr(Attribute::Borders, AttrValue::Borders(b));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MockComponent for Rooms {
|
||||
fn view(&mut self, frame: &mut Frame<'_>, area: Rect) {
|
||||
let title = ("Rooms".to_owned(), Alignment::Center);
|
||||
|
||||
let borders = self
|
||||
.props
|
||||
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
|
||||
.unwrap_borders();
|
||||
let focus = self.props.get_or(Attribute::Focus, AttrValue::Flag(false)).unwrap_flag();
|
||||
|
||||
let mut paras = vec![];
|
||||
|
||||
for r in self.sstate.get_all_rooms() {
|
||||
let mut cells = vec![Cell::from(r.name().unwrap_or("unknown").to_owned())];
|
||||
if let Some(c) = r.unread_notifications().notification_count {
|
||||
let count: u32 = c.try_into().unwrap_or_default();
|
||||
if count > 0 {
|
||||
cells.push(Cell::from(c.to_string()))
|
||||
}
|
||||
}
|
||||
paras.push(Row::new(cells));
|
||||
}
|
||||
|
||||
frame.render_stateful_widget(
|
||||
Table::new(paras)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.highlight_style(
|
||||
Style::default().fg(Color::LightCyan).add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.highlight_symbol(">>")
|
||||
.widths(&[Constraint::Min(30), Constraint::Max(4)])
|
||||
.block(get_block(borders, title, focus)),
|
||||
area,
|
||||
&mut self.tablestate,
|
||||
);
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::None
|
||||
}
|
||||
|
||||
fn perform(&mut self, _: Cmd) -> CmdResult {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, JackInEvent> for Rooms {
|
||||
fn on(&mut self, ev: Event<JackInEvent>) -> Option<Msg> {
|
||||
let focus = self.props.get_or(Attribute::Focus, AttrValue::Flag(false)).unwrap_flag();
|
||||
if focus {
|
||||
// we only care about user input if we are in focus.
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Down, modifiers: KeyModifiers::NONE }) => {
|
||||
self.select_dir(1);
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Down, modifiers: KeyModifiers::SHIFT }) => {
|
||||
self.select_dir(10);
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, modifiers: KeyModifiers::NONE }) => {
|
||||
self.select_dir(-1);
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, modifiers: KeyModifiers::SHIFT }) => {
|
||||
self.select_dir(-10);
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Enter, modifiers: KeyModifiers::NONE }) => {
|
||||
if let Some(idx) = self.tablestate.selected() {
|
||||
if let Some(room_id) = self.sstate.view().get_room_id(idx) {
|
||||
warn!("selecting, {:?}", room_id);
|
||||
return Some(Msg::SelectRoom(Some(room_id)));
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Tab, modifiers: KeyModifiers::NONE }) => {
|
||||
return Some(Msg::RoomsBlur)
|
||||
} // Return focus lost
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, modifiers: KeyModifiers::NONE }) => {
|
||||
return Some(Msg::AppClose)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Event::User(JackInEvent::SyncUpdate(s)) = ev {
|
||||
self.set_sliding_sync(s);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
use tuirealm::{
|
||||
command::{Cmd, CmdResult},
|
||||
props::{Alignment, Borders, Color, Style},
|
||||
tui::{layout::Rect, text::Spans, widgets::Tabs},
|
||||
AttrValue, Attribute, Component, Event, Frame, MockComponent, Props, State,
|
||||
};
|
||||
|
||||
use super::{super::client::state::SlidingSyncState, get_block, JackInEvent, Msg};
|
||||
|
||||
/// ## StatusBar
|
||||
pub struct StatusBar {
|
||||
props: Props,
|
||||
sstate: SlidingSyncState,
|
||||
}
|
||||
|
||||
impl StatusBar {
|
||||
pub fn new(sstate: SlidingSyncState) -> Self {
|
||||
Self { props: Props::default(), sstate }
|
||||
}
|
||||
pub fn set_sliding_sync(&mut self, sstate: SlidingSyncState) {
|
||||
self.sstate = sstate;
|
||||
}
|
||||
}
|
||||
|
||||
impl MockComponent for StatusBar {
|
||||
fn view(&mut self, frame: &mut Frame<'_>, area: Rect) {
|
||||
let title = ("Status".to_owned(), Alignment::Left);
|
||||
|
||||
let borders = self
|
||||
.props
|
||||
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
|
||||
.unwrap_borders();
|
||||
|
||||
let focus = self.props.get_or(Attribute::Focus, AttrValue::Flag(false)).unwrap_flag();
|
||||
|
||||
let tabs = {
|
||||
let mut tabs =
|
||||
vec![Spans::from(format!("Current state: {:?}", self.sstate.current_state()))];
|
||||
if let Some(dur) = self.sstate.time_to_first_render() {
|
||||
tabs.push(Spans::from(format!("First view: {}ms", dur.as_millis())));
|
||||
|
||||
if let Some(dur) = self.sstate.time_to_full_sync() {
|
||||
tabs.push(Spans::from(format!("Full sync: {}ms", dur.as_millis())));
|
||||
if let Some(count) = self.sstate.total_rooms_count() {
|
||||
tabs.push(Spans::from(format!("{count} rooms")));
|
||||
}
|
||||
} else {
|
||||
tabs.push(Spans::from(format!(
|
||||
"Loaded {} rooms in {}s",
|
||||
self.sstate.loaded_rooms_count(),
|
||||
self.sstate.started().elapsed().as_secs()
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
tabs.push(Spans::from(format!(
|
||||
"loading for {}s",
|
||||
self.sstate.started().elapsed().as_secs()
|
||||
)));
|
||||
}
|
||||
tabs
|
||||
};
|
||||
|
||||
frame.render_widget(
|
||||
Tabs::new(tabs)
|
||||
.style(Style::default().fg(Color::LightCyan))
|
||||
.block(get_block(borders, title, focus))
|
||||
.style(Style::default().fg(Color::White).bg(Color::Black)),
|
||||
area,
|
||||
);
|
||||
}
|
||||
|
||||
fn query(&self, attr: Attribute) -> Option<AttrValue> {
|
||||
self.props.get(attr)
|
||||
}
|
||||
|
||||
fn attr(&mut self, attr: Attribute, value: AttrValue) {
|
||||
self.props.set(attr, value);
|
||||
}
|
||||
|
||||
fn state(&self) -> State {
|
||||
State::None
|
||||
}
|
||||
|
||||
fn perform(&mut self, _: Cmd) -> CmdResult {
|
||||
CmdResult::None
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, JackInEvent> for StatusBar {
|
||||
fn on(&mut self, ev: Event<JackInEvent>) -> Option<Msg> {
|
||||
if let Event::User(JackInEvent::SyncUpdate(s)) = ev {
|
||||
self.set_sliding_sync(s);
|
||||
None
|
||||
} else if let Event::Tick = ev {
|
||||
Some(Msg::Clock)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Args, Parser, ValueEnum};
|
||||
use matrix_sdk::SlidingSyncMode;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "jack-in", about = "Your experimental sliding-sync jack into the matrix")]
|
||||
pub struct Opt {
|
||||
/// The password of your account. If not given and no database found, it
|
||||
/// will prompt you for it
|
||||
#[arg(short, long, env = "JACKIN_PASSWORD")]
|
||||
pub password: Option<String>,
|
||||
|
||||
/// Create a fresh database, drop all existing cache
|
||||
#[arg(long)]
|
||||
pub fresh: bool,
|
||||
|
||||
/// RUST_LOG log-levels
|
||||
#[arg(short, long, env = "JACKIN_LOG", default_value = "jack_in=info,warn")]
|
||||
pub log: String,
|
||||
|
||||
/// The userID to log in with
|
||||
#[arg(short, long, env = "JACKIN_USER")]
|
||||
pub user: String,
|
||||
|
||||
/// The password to encrypt the store with
|
||||
#[arg(long, env = "JACKIN_STORE_PASSWORD")]
|
||||
pub store_pass: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
/// Activate tracing and write the flamegraph to the specified file
|
||||
pub flames: Option<PathBuf>,
|
||||
|
||||
#[command(flatten)]
|
||||
/// Sliding Sync configuration flags
|
||||
pub sliding_sync: SlidingSyncConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
#[value(rename_all = "lower")]
|
||||
pub enum FullSyncMode {
|
||||
Growing,
|
||||
Paging,
|
||||
}
|
||||
|
||||
impl From<FullSyncMode> for SlidingSyncMode {
|
||||
fn from(val: FullSyncMode) -> Self {
|
||||
match val {
|
||||
FullSyncMode::Growing => SlidingSyncMode::GrowingFullSync,
|
||||
FullSyncMode::Paging => SlidingSyncMode::PagingFullSync,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct SlidingSyncConfig {
|
||||
/// The address of the sliding sync server to connect (probs the proxy)
|
||||
#[arg(
|
||||
long = "sliding-sync-proxy",
|
||||
default_value = "http://localhost:8008",
|
||||
env = "JACKIN_SYNC_PROXY"
|
||||
)]
|
||||
pub proxy: String,
|
||||
|
||||
/// Activate growing window rather than pagination for full-sync
|
||||
#[arg(long, default_value = "paging")]
|
||||
pub full_sync_mode: FullSyncMode,
|
||||
|
||||
/// Limit the growing/paging to this number of maximum items to caonsider
|
||||
/// "done"
|
||||
#[arg(long)]
|
||||
pub limit: Option<u32>,
|
||||
|
||||
/// define the batch_size per request
|
||||
#[arg(long)]
|
||||
pub batch_size: Option<u32>,
|
||||
|
||||
/// define the timeline items to load
|
||||
#[arg(long)]
|
||||
pub timeline_limit: Option<u32>,
|
||||
}
|
|
@ -1,298 +0,0 @@
|
|||
//! ## Jack-in
|
||||
//!
|
||||
//! a demonstration and debugging implementation TUI client for sliding sync
|
||||
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use app_dirs2::{app_root, AppDataType, AppInfo};
|
||||
use clap::Parser;
|
||||
use dialoguer::{theme::ColorfulTheme, Password};
|
||||
use eyeball_im::VectorDiff;
|
||||
use eyre::{eyre, Result};
|
||||
use matrix_sdk::{
|
||||
config::RequestConfig,
|
||||
room::timeline::TimelineItem,
|
||||
ruma::{OwnedRoomId, OwnedUserId},
|
||||
Client,
|
||||
};
|
||||
use matrix_sdk_sled::make_store_config;
|
||||
use sanitize_filename_reader_friendly::sanitize;
|
||||
use tracing::{error, info, log};
|
||||
use tracing_flame::FlameLayer;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tuirealm::{application::PollStrategy, Event, Update};
|
||||
|
||||
const APP_INFO: AppInfo = AppInfo { name: "jack-in", author: "Matrix-Rust-SDK Core Team" };
|
||||
|
||||
// -- internal
|
||||
mod app;
|
||||
mod client;
|
||||
mod components;
|
||||
mod config;
|
||||
use app::model::Model;
|
||||
use config::{Opt, SlidingSyncConfig};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// Let's define the messages handled by our app. NOTE: it must derive
|
||||
// `PartialEq`
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Msg {
|
||||
AppClose,
|
||||
Clock,
|
||||
RoomsBlur,
|
||||
DetailsBlur,
|
||||
TextBlur,
|
||||
SelectRoom(Option<OwnedRoomId>),
|
||||
SendMessage(String),
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone)]
|
||||
pub enum JackInEvent {
|
||||
Any, // match all
|
||||
SyncUpdate(client::state::SlidingSyncState),
|
||||
RoomDataUpdate(VectorDiff<TimelineItem>),
|
||||
}
|
||||
|
||||
impl PartialOrd for JackInEvent {
|
||||
fn partial_cmp(&self, _other: &Self) -> Option<std::cmp::Ordering> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for JackInEvent {}
|
||||
|
||||
impl PartialEq for JackInEvent {
|
||||
fn eq(&self, _other: &JackInEvent) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Let's define the component ids for our application
|
||||
#[derive(Debug, Eq, PartialEq, PartialOrd, Clone, Hash)]
|
||||
pub enum Id {
|
||||
Clock,
|
||||
DigitCounter,
|
||||
LetterCounter,
|
||||
Label,
|
||||
TextMessage,
|
||||
Logger,
|
||||
Status,
|
||||
Rooms,
|
||||
Details,
|
||||
}
|
||||
|
||||
pub(crate) struct MatrixPoller(mpsc::Receiver<client::state::SlidingSyncState>);
|
||||
|
||||
impl tuirealm::listener::Poll<JackInEvent> for MatrixPoller {
|
||||
fn poll(&mut self) -> tuirealm::listener::ListenerResult<Option<Event<JackInEvent>>> {
|
||||
match self.0.try_recv() {
|
||||
Ok(v) => Ok(Some(Event::User(JackInEvent::SyncUpdate(v)))),
|
||||
Err(mpsc::error::TryRecvError::Empty) => Ok(None),
|
||||
_ => Err(tuirealm::listener::ListenerError::ListenerDied),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_flames(path: &Path) -> impl Drop {
|
||||
let (flame_layer, _guard) = FlameLayer::with_file(path).expect("Couldn't write flamegraph");
|
||||
|
||||
tracing_subscriber::registry().with(flame_layer).init();
|
||||
_guard
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let opt = Opt::parse();
|
||||
|
||||
let user_id: OwnedUserId = opt.user.clone().parse()?;
|
||||
|
||||
if let Some(ref p) = opt.flames {
|
||||
setup_flames(p.as_path());
|
||||
} else {
|
||||
// Configure log
|
||||
|
||||
//tracing_subscriber::fmt::init();
|
||||
#[cfg(feature = "file-logging")]
|
||||
{
|
||||
use log4rs::{
|
||||
append::file::FileAppender,
|
||||
config::{Appender, Config, Logger, Root},
|
||||
encode::pattern::PatternEncoder,
|
||||
};
|
||||
|
||||
let file = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::default()))
|
||||
.build("jack-in.log")
|
||||
.unwrap();
|
||||
|
||||
let config = Config::builder()
|
||||
.appender(Appender::builder().build("file", Box::new(file)))
|
||||
.logger(
|
||||
Logger::builder()
|
||||
.appender("file")
|
||||
.build("matrix_sdk::sliding_sync", log::LevelFilter::Trace),
|
||||
)
|
||||
.logger(
|
||||
Logger::builder()
|
||||
.appender("file")
|
||||
.build("matrix_sdk::http_client", log::LevelFilter::Debug),
|
||||
)
|
||||
.logger(
|
||||
Logger::builder()
|
||||
.appender("file")
|
||||
.build("matrix_sdk_base::sliding_sync", log::LevelFilter::Debug),
|
||||
)
|
||||
.logger(
|
||||
Logger::builder().appender("file").build("reqwest", log::LevelFilter::Trace),
|
||||
)
|
||||
.logger(
|
||||
Logger::builder().appender("file").build("matrix_sdk", log::LevelFilter::Warn),
|
||||
)
|
||||
.build(Root::builder().build(log::LevelFilter::Error))
|
||||
.unwrap();
|
||||
|
||||
log4rs::init_config(config).expect("Logging with log4rs failed to initialize");
|
||||
}
|
||||
#[cfg(not(feature = "file-logging"))]
|
||||
{
|
||||
tui_logger::init_logger(log::LevelFilter::Trace).unwrap();
|
||||
// Set default level for unknown targets to Trace
|
||||
tui_logger::set_default_level(log::LevelFilter::Warn);
|
||||
|
||||
for pair in opt.log.split(',') {
|
||||
if let Some((name, lvl)) = pair.split_once('=') {
|
||||
let level = match lvl.to_lowercase().as_str() {
|
||||
"trace" => log::LevelFilter::Trace,
|
||||
"debug" => log::LevelFilter::Debug,
|
||||
"info" => log::LevelFilter::Info,
|
||||
"warn" => log::LevelFilter::Warn,
|
||||
"error" => log::LevelFilter::Error,
|
||||
// nothing means error
|
||||
_ => continue,
|
||||
};
|
||||
tui_logger::set_level_for_target(name, level);
|
||||
} else {
|
||||
let level = match pair.to_lowercase().as_str() {
|
||||
"trace" => log::LevelFilter::Trace,
|
||||
"debug" => log::LevelFilter::Debug,
|
||||
"info" => log::LevelFilter::Info,
|
||||
"warn" => log::LevelFilter::Warn,
|
||||
"error" => log::LevelFilter::Error,
|
||||
// nothing means error
|
||||
_ => continue,
|
||||
};
|
||||
tui_logger::set_default_level(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let data_path = app_root(AppDataType::UserData, &APP_INFO)?.join(sanitize(user_id.as_str()));
|
||||
if opt.fresh {
|
||||
// drop the database first;
|
||||
std::fs::remove_dir_all(&data_path)?;
|
||||
}
|
||||
std::fs::create_dir_all(&data_path)?;
|
||||
let store_config = make_store_config(&data_path, opt.store_pass.as_deref()).await?;
|
||||
let request_config = RequestConfig::default().timeout(Duration::from_secs(90));
|
||||
|
||||
let client = Client::builder()
|
||||
.user_agent("jack-in")
|
||||
.server_name(user_id.server_name())
|
||||
.request_config(request_config)
|
||||
.store_config(store_config)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let session_key = b"jackin::session_token";
|
||||
|
||||
if let Some(session) = client
|
||||
.store()
|
||||
.get_custom_value(session_key)
|
||||
.await?
|
||||
.map(|v| serde_json::from_slice(&v))
|
||||
.transpose()?
|
||||
{
|
||||
info!("Restoring session from store");
|
||||
client.restore_session(session).await?;
|
||||
} else {
|
||||
let theme = ColorfulTheme::default();
|
||||
let password = match opt.password {
|
||||
Some(ref pw) => pw.clone(),
|
||||
_ => Password::with_theme(&theme)
|
||||
.with_prompt(format!("Password for {user_id:} :"))
|
||||
.interact()?,
|
||||
};
|
||||
client.login_username(&user_id, &password).await?;
|
||||
}
|
||||
|
||||
if let Some(session) = client.session() {
|
||||
client.store().set_custom_value(session_key, serde_json::to_vec(&session)?).await?;
|
||||
}
|
||||
|
||||
let sliding_client = client.clone();
|
||||
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
let model_tx = tx.clone();
|
||||
let sliding_sync_proxy = opt.sliding_sync.proxy.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = client::run_client(sliding_client, tx, opt.sliding_sync).await {
|
||||
error!("Running the client failed: {:#?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let start_sync =
|
||||
rx.recv().await.ok_or_else(|| eyre!("failure getting the sliding sync state"))?;
|
||||
// ensure client still works as normal: fetch user info:
|
||||
let display_name = client
|
||||
.account()
|
||||
.get_display_name()
|
||||
.await?
|
||||
.map(|s| format!("{s} ({user_id})"))
|
||||
.unwrap_or_else(|| format!("{user_id}"));
|
||||
let poller = MatrixPoller(rx);
|
||||
let mut model = Model::new(start_sync, model_tx, poller, client);
|
||||
model.set_title(format!("{display_name} via {sliding_sync_proxy}"));
|
||||
run_ui(model).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_ui(mut model: Model) {
|
||||
// Enter alternate screen
|
||||
let _ = model.terminal.enter_alternate_screen();
|
||||
let _ = model.terminal.enable_raw_mode();
|
||||
// Main loop
|
||||
// NOTE: loop until quit; quit is set in update if AppClose is received from
|
||||
// counter
|
||||
while !model.quit {
|
||||
// Tick
|
||||
match model.app.tick(PollStrategy::Once) {
|
||||
Err(err) => {
|
||||
model.set_title(format!("Application error: {err}"));
|
||||
}
|
||||
Ok(messages) if !messages.is_empty() => {
|
||||
// NOTE: redraw if at least one msg has been processed
|
||||
model.redraw = true;
|
||||
for msg in messages.into_iter() {
|
||||
let mut msg = Some(msg);
|
||||
while msg.is_some() {
|
||||
msg = model.update(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Redraw
|
||||
if model.redraw {
|
||||
model.view();
|
||||
model.redraw = false;
|
||||
}
|
||||
}
|
||||
// Terminate terminal
|
||||
let _ = model.terminal.leave_alternate_screen();
|
||||
let _ = model.terminal.disable_raw_mode();
|
||||
let _ = model.terminal.clear_screen();
|
||||
}
|
|
@ -21,8 +21,6 @@ exclude = [
|
|||
# testing
|
||||
"matrix-sdk-test",
|
||||
"matrix-sdk-test-macros",
|
||||
# labs
|
||||
"jack-in",
|
||||
# repo automation (ci, codegen)
|
||||
"uniffi-bindgen",
|
||||
"xtask",
|
||||
|
|
Loading…
Reference in New Issue