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:
Ivan Enderlin 2023-03-30 18:03:38 +02:00
parent 7dd086fcdc
commit ea5b4c98c1
No known key found for this signature in database
17 changed files with 1 additions and 2219 deletions

356
Cargo.lock generated
View File

@ -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"

View File

@ -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 }

View File

@ -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

View File

@ -1,2 +0,0 @@
pub use super::*;
pub mod model;

View File

@ -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
}
}
}

View File

@ -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(())
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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>,
}

View File

@ -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();
}

View File

@ -21,8 +21,6 @@ exclude = [
# testing
"matrix-sdk-test",
"matrix-sdk-test-macros",
# labs
"jack-in",
# repo automation (ci, codegen)
"uniffi-bindgen",
"xtask",