1#![allow(clippy::large_enum_variant)]
2
3use std::{
4 collections::{HashMap, HashSet},
5 io::{self, Write, stdout},
6 path::{Path, PathBuf},
7 sync::Arc,
8 time::{Duration, Instant},
9};
10
11use clap::Parser;
12use color_eyre::Result;
13use crossterm::{
14 event::{
15 self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyModifiers,
16 },
17 execute,
18};
19use futures_util::{StreamExt as _, pin_mut};
20use imbl::Vector;
21use layout::Flex;
22use matrix_sdk::{
23 AuthSession, Client, SqliteCryptoStore, SqliteEventCacheStore, SqliteStateStore,
24 ThreadingSupport,
25 authentication::matrix::MatrixSession,
26 config::StoreConfig,
27 encryption::{BackupDownloadStrategy, EncryptionSettings},
28 reqwest::Url,
29 ruma::{OwnedRoomId, api::client::room::create_room::v3::Request as CreateRoomRequest},
30 search_index::{SearchIndexGuard, SearchIndexStoreKind},
31};
32use matrix_sdk_base::{RoomStateFilter, event_cache::store::EventCacheStoreLockGuard};
33use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, locks::Mutex};
34use matrix_sdk_ui::{
35 Timeline as SdkTimeline,
36 room_list_service::{self, State, filters::new_filter_non_left},
37 sync_service::SyncService,
38 timeline::{RoomExt as _, TimelineFocus, TimelineItem},
39};
40use ratatui::{prelude::*, style::palette::tailwind, widgets::*};
41use throbber_widgets_tui::{Throbber, ThrobberState};
42use tokio::{
43 spawn,
44 sync::mpsc::{Receiver, Sender, channel, error::TryRecvError},
45 task::JoinHandle,
46 time::timeout,
47};
48use tracing::{debug, error, warn};
49use tracing_subscriber::EnvFilter;
50use widgets::{
51 recovery::create_centered_throbber_area, room_view::RoomView, settings::SettingsView,
52};
53
54use crate::widgets::{
55 create_room::CreateRoomView,
56 help::HelpView,
57 room_list::{ExtraRoomInfo, RoomInfos, RoomList, Rooms},
58 search::{
59 indexing::{IndexingMessage, IndexingView},
60 searching::SearchingView,
61 },
62 status::Status,
63};
64
65mod widgets;
66
67const HEADER_BG: Color = tailwind::BLUE.c950;
68const NORMAL_ROW_COLOR: Color = tailwind::SLATE.c950;
69const ALT_ROW_COLOR: Color = tailwind::SLATE.c900;
70const SELECTED_STYLE_FG: Color = tailwind::BLUE.c300;
71const TEXT_COLOR: Color = tailwind::SLATE.c200;
72
73type Timelines = Arc<Mutex<HashMap<OwnedRoomId, Timeline>>>;
74
75#[derive(Debug, Parser)]
76struct Cli {
77 server_name: String,
79
80 #[clap(default_value = "/tmp/")]
82 session_path: PathBuf,
83
84 #[clap(short, long, env = "PROXY")]
86 proxy: Option<Url>,
87
88 #[clap(short, long, default_value_t = false)]
93 dont_share_pos: bool,
94}
95
96#[derive(Default)]
97pub enum GlobalMode {
98 #[default]
100 Default,
101 Help,
103 Settings { view: SettingsView },
105 Exiting { shutdown_task: JoinHandle<()> },
107 CreateRoom { view: CreateRoomView },
109 Searching { view: SearchingView, is_global: bool },
111 Indexing { view: IndexingView },
113}
114
115fn popup_area(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
118 let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
119 let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
120 let [area] = vertical.areas(area);
121 let [area] = horizontal.areas(area);
122 area
123}
124
125#[tokio::main]
126async fn main() -> Result<()> {
127 let cli = Cli::parse();
128 let file_writer = tracing_appender::rolling::hourly(&cli.session_path, "logs-");
129
130 tracing_subscriber::fmt()
131 .with_env_filter(EnvFilter::from_default_env())
132 .with_ansi(false)
133 .with_writer(file_writer)
134 .init();
135
136 color_eyre::install()?;
137
138 let share_pos = !cli.dont_share_pos;
139 let client = configure_client(cli).await?;
140
141 spawn({
143 let client = client.clone();
144 async move {
145 let mut recv = client.task_monitor().subscribe();
146 while let Ok(report) = recv.recv().await {
147 error!(name = report.task.name, ?report.reason, "A background task has crashed!");
148 }
149 }
150 });
151
152 let event_cache = client.event_cache();
153 event_cache.config_mut().experimental_auto_backpagination = true;
154 event_cache.subscribe()?;
155
156 let terminal = ratatui::init();
157 execute!(stdout(), EnableMouseCapture)?;
158 let mut app = App::new(client, share_pos).await?;
159
160 app.run(terminal).await
161}
162
163pub struct Timeline {
164 timeline: Arc<SdkTimeline>,
165 items: Arc<Mutex<Vector<Arc<TimelineItem>>>>,
166 task: JoinHandle<()>,
167}
168
169#[derive(Default)]
170pub struct AppState {
171 global_mode: GlobalMode,
174
175 throbber_state: ThrobberState,
177}
178
179struct App {
180 client: Client,
182
183 sync_service: Arc<SyncService>,
185
186 timelines: Timelines,
188
189 room_list: RoomList,
191
192 room_view: RoomView,
195
196 listen_task: JoinHandle<()>,
198
199 indexing_task: JoinHandle<()>,
201
202 indexing_receiver: Receiver<(bool, IndexingMessage)>,
204
205 status: Status,
207
208 state: AppState,
209
210 last_tick: Instant,
211}
212
213impl App {
214 const TICK_RATE: Duration = Duration::from_millis(250);
215
216 async fn new(client: Client, share_pos: bool) -> Result<Self> {
217 let sync_service =
218 Arc::new(SyncService::builder(client.clone()).with_share_pos(share_pos).build().await?);
219
220 let rooms = Rooms::default();
221 let room_infos = RoomInfos::default();
222 let timelines = Timelines::default();
223
224 let room_list_service = sync_service.room_list_service();
225 let all_rooms = room_list_service.all_rooms().await?;
226
227 let listen_task = spawn(Self::listen_task(
228 rooms.clone(),
229 room_infos.clone(),
230 timelines.clone(),
231 all_rooms,
232 ));
233
234 sync_service.start().await;
237
238 let status = Status::new();
239 let room_list =
240 RoomList::new(client.clone(), rooms, room_infos, sync_service.clone(), status.handle());
241
242 let room_view = RoomView::new(client.clone(), timelines.clone(), status.handle());
243
244 let (indexing_sender, indexing_receiver) = channel::<(bool, IndexingMessage)>(1024);
245 let indexing_task =
246 spawn(App::indexing_task(client.clone(), indexing_sender, sync_service.clone()));
247
248 let indexing_view = IndexingView::new();
249
250 Ok(Self {
251 sync_service,
252 timelines,
253 room_list,
254 room_view,
255 client,
256 listen_task,
257 indexing_task,
258 indexing_receiver,
259 status,
260 state: AppState {
261 global_mode: GlobalMode::Indexing { view: indexing_view },
262 ..Default::default()
263 },
264 last_tick: Instant::now(),
265 })
266 }
267
268 async fn listen_task(
269 rooms: Rooms,
270 room_infos: RoomInfos,
271 timelines: Timelines,
272 all_rooms: room_list_service::RoomList,
273 ) {
274 let (stream, entries_controller) = all_rooms.entries_with_dynamic_adapters(50_000);
275 entries_controller.set_filter(Box::new(new_filter_non_left()));
276
277 pin_mut!(stream);
278
279 let mut previous_rooms = HashSet::new();
280
281 while let Some(diffs) = stream.next().await {
282 let all_rooms = {
283 let mut rooms = rooms.lock();
285
286 for diff in diffs {
287 diff.apply(&mut rooms);
288 }
289
290 (*rooms).clone()
292 };
293
294 let mut new_rooms = HashMap::new();
295 let mut new_timelines = Vec::new();
296
297 for room in all_rooms.iter() {
299 let raw_name = room.name();
300 let display_name =
301 room.cached_display_name().map(|display_name| display_name.to_string());
302 let is_dm = room
303 .is_direct()
304 .await
305 .map_err(|err| {
306 warn!("couldn't figure whether a room is a DM or not: {err}");
307 })
308 .ok();
309 room_infos.lock().insert(
310 room.room_id().to_owned(),
311 ExtraRoomInfo { raw_name, display_name, is_dm },
312 );
313 }
314
315 for room in
317 all_rooms.into_iter().filter(|room| !previous_rooms.contains(room.room_id()))
318 {
319 let Ok(timeline) = room
321 .timeline_builder()
322 .with_focus(TimelineFocus::Live { hide_threaded_events: true })
323 .build()
324 .await
325 else {
326 error!("error when creating default timeline");
327 continue;
328 };
329
330 let (items, stream) = timeline.subscribe().await;
332 let items = Arc::new(Mutex::new(items));
333
334 let i = items.clone();
336 let timeline_task = spawn(async move {
337 pin_mut!(stream);
338 let items = i;
339 while let Some(diffs) = stream.next().await {
340 let mut items = items.lock();
341
342 for diff in diffs {
343 diff.apply(&mut items);
344 }
345 }
346 });
347
348 new_timelines.push((
349 room.room_id().to_owned(),
350 Timeline { timeline: Arc::new(timeline), items, task: timeline_task },
351 ));
352
353 new_rooms.insert(room.room_id().to_owned(), room);
355 }
356
357 previous_rooms.extend(new_rooms.into_keys());
358
359 timelines.lock().extend(new_timelines);
360 }
361 }
362
363 async fn wait_for_room_sync(
364 update_sender: &Sender<(bool, IndexingMessage)>,
365 sync_service: Arc<SyncService>,
366 ) {
367 let mut sync_subscriber = sync_service.room_list_service().state();
368
369 while let Some(state) = sync_subscriber.next().await {
371 match state {
372 State::Running => return,
373 State::Terminated { from: _prev } => {
374 while let Err(e) =
375 update_sender.send((true, IndexingMessage::Progress(0))).await
376 {
377 debug!("Failed to send final message, trying again: {e:?}");
378 }
379 return;
380 }
381 _ => {
382 debug!("Sync service not running. Waiting to start indexing. {state:?}");
383 }
384 }
385 }
386 }
387
388 async fn index_event_cache(
389 client: &Client,
390 update_sender: &Sender<(bool, IndexingMessage)>,
391 store: &EventCacheStoreLockGuard,
392 search_index_guard: &mut SearchIndexGuard<'_>,
393 mut count: usize,
394 ) -> Result<usize, ()> {
395 for room in client.rooms_filtered(RoomStateFilter::JOINED.union(RoomStateFilter::LEFT)) {
396 let room_id = room.room_id();
397
398 let maybe_room_cache = room.event_cache().await;
399 let Ok((room_cache, _drop_handles)) = maybe_room_cache else {
400 warn!("Failed to get RoomEventCache: {maybe_room_cache:?}");
401 continue;
402 };
403
404 let redaction_rules = room.clone_info().room_version_rules_or_default().redaction;
405
406 let maybe_timeline_events = store.get_room_events(room_id, None, None).await;
407 let Ok(timeline_events) = maybe_timeline_events else {
408 warn!("Failed to get room's events: {maybe_timeline_events:?}");
409 continue;
410 };
411
412 let no_of_events = timeline_events.len();
413
414 if let Err(err) = search_index_guard
415 .bulk_handle_timeline_event(
416 timeline_events.clone().into_iter(),
417 &room_cache,
418 room_id,
419 &redaction_rules,
420 )
421 .await
422 {
423 error!("Failed to handle event for indexing: {err}");
424 let mut error = Some(err);
425 while let Some(err) = error.take() {
426 if let Err(e) =
427 update_sender.send((true, IndexingMessage::Error(err.to_string()))).await
428 {
429 debug!("Failed to send final error message, trying again: {e:?}");
430 }
431 }
432 return Err(());
433 }
434
435 count += no_of_events;
436 let _ = update_sender.send((false, IndexingMessage::Progress(count))).await;
437 }
438 Ok(count)
439 }
440
441 async fn index_from_server(
442 client: &Client,
443 update_sender: &Sender<(bool, IndexingMessage)>,
444 search_index_guard: &mut SearchIndexGuard<'_>,
445 mut count: usize,
446 ) -> Result<usize, ()> {
447 let batch_size = 25;
448
449 let mut rooms = client.rooms_filtered(RoomStateFilter::JOINED);
450 let mut idx = 0;
451
452 while !rooms.is_empty() {
453 let room = &rooms[idx];
454
455 let room_id = room.room_id();
456
457 let maybe_room_cache = room.event_cache().await;
458 let Ok((room_cache, _drop_handles)) = maybe_room_cache else {
459 warn!("Failed to get RoomEventCache: {maybe_room_cache:?}");
460 idx = (idx + 1) % rooms.len();
461 continue;
462 };
463
464 let redaction_rules = room.clone_info().room_version_rules_or_default().redaction;
465
466 let Ok(pagination) = room_cache.pagination().run_backwards_until(batch_size).await
467 else {
468 error!("Failed to backpaginate {room_id}");
469 idx = (idx + 1) % rooms.len();
470 continue;
471 };
472
473 let no_of_events = pagination.events.len();
474
475 if let Err(err) = search_index_guard
476 .bulk_handle_timeline_event(
477 pagination.events.clone().into_iter(),
478 &room_cache,
479 room_id,
480 &redaction_rules,
481 )
482 .await
483 {
484 warn!("Failed to handle event for indexing: {err}");
485 let mut error = Some(err);
486 while let Some(err) = error.take() {
487 if let Err(e) =
488 update_sender.send((true, IndexingMessage::Error(err.to_string()))).await
489 {
490 debug!("Failed to send final error message, trying again: {e:?}");
491 }
492 }
493 return Err(());
494 }
495
496 count += no_of_events;
497 let _ = update_sender.send((false, IndexingMessage::Progress(count))).await;
498
499 if pagination.reached_start {
500 rooms.remove(idx);
501 let len = rooms.len();
502 if len > 0 {
503 idx %= len;
504 }
505 } else {
506 idx = (idx + 1) % rooms.len();
507 }
508 }
509 Ok(count)
510 }
511
512 async fn indexing_task(
514 client: Client,
515 update_sender: Sender<(bool, IndexingMessage)>,
516 sync_service: Arc<SyncService>,
517 ) {
518 if timeout(Duration::from_secs(30), App::wait_for_room_sync(&update_sender, sync_service))
519 .await
520 .is_err()
521 {
522 debug!("Waiting for sync to run timed out. Quitting indexing task.");
523 return;
524 }
525
526 let Ok(store) = client.event_cache_store().lock().await else {
527 error!("Failed to get EventCacheStore");
528 return;
529 };
530
531 let mut search_index_guard = client.search_index().lock().await;
532 let count = 0;
533
534 debug!("Start indexing from the event cache.");
535
536 let Ok(count) = App::index_event_cache(
538 &client,
539 &update_sender,
540 store.as_clean().expect("Only one process should access the event cache store"),
541 &mut search_index_guard,
542 count,
543 )
544 .await
545 else {
546 debug!("Quitting index task.");
547 return;
548 };
549
550 debug!("Start indexing from the server.");
552
553 let Ok(count) =
554 App::index_from_server(&client, &update_sender, &mut search_index_guard, count).await
555 else {
556 debug!("Quitting index task.");
557 return;
558 };
559
560 while let Err(err) = update_sender.send((true, IndexingMessage::Progress(count))).await {
561 debug!("couldn't send final update {err}, trying again.");
562 }
563 }
564
565 fn set_global_mode(&mut self, mode: GlobalMode) {
566 self.state.global_mode = mode;
567 }
568
569 async fn handle_global_event(&mut self, event: Event) -> Result<bool> {
570 use KeyCode::*;
571
572 match event {
573 Event::Key(KeyEvent { code: F(1), modifiers: KeyModifiers::NONE, .. }) => {
574 self.set_global_mode(GlobalMode::Help)
575 }
576
577 Event::Key(KeyEvent { code: F(10), modifiers: KeyModifiers::NONE, .. }) => self
578 .set_global_mode(GlobalMode::Settings {
579 view: SettingsView::new(self.client.clone(), self.sync_service.clone()),
580 }),
581
582 Event::Key(KeyEvent {
583 code: Char('j') | Down,
584 modifiers: KeyModifiers::CONTROL,
585 ..
586 }) => {
587 self.room_list.next_room().await;
588 let room_id = self.room_list.get_selected_room_id();
589 self.room_view.set_selected_room(room_id);
590 }
591
592 Event::Key(KeyEvent {
593 code: Char('k') | Up, modifiers: KeyModifiers::CONTROL, ..
594 }) => {
595 self.room_list.previous_room().await;
596 let room_id = self.room_list.get_selected_room_id();
597 self.room_view.set_selected_room(room_id);
598 }
599
600 Event::Key(KeyEvent { code: Char('m'), modifiers: KeyModifiers::ALT, .. }) => {
601 self.room_view.mark_as_read().await
602 }
603
604 Event::Key(KeyEvent { code: Char('q'), modifiers: KeyModifiers::CONTROL, .. }) => {
605 if !matches!(self.state.global_mode, GlobalMode::Default) {
606 self.set_global_mode(GlobalMode::Default);
607 } else {
608 return Ok(true);
609 }
610 }
611
612 Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('r'), .. }) => {
613 self.set_global_mode(GlobalMode::CreateRoom { view: CreateRoomView::new() })
614 }
615
616 Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('s'), .. }) => self
617 .set_global_mode(GlobalMode::Searching {
618 view: SearchingView::new(false),
619 is_global: false,
620 }),
621
622 Event::Key(KeyEvent { modifiers: KeyModifiers::CONTROL, code: Char('g'), .. }) => self
623 .set_global_mode(GlobalMode::Searching {
624 view: SearchingView::new(true),
625 is_global: true,
626 }),
627
628 _ => self.room_view.handle_event(event).await,
629 }
630
631 Ok(false)
632 }
633
634 fn on_tick(&mut self) {
635 self.state.throbber_state.calc_next();
636
637 match &mut self.state.global_mode {
638 GlobalMode::Help
639 | GlobalMode::Default
640 | GlobalMode::CreateRoom { .. }
641 | GlobalMode::Searching { .. }
642 | GlobalMode::Exiting { .. } => {}
643 GlobalMode::Settings { view } => {
644 view.on_tick();
645 }
646 GlobalMode::Indexing { view } => {
647 view.on_tick();
648 }
649 }
650 }
651
652 async fn render_loop(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
653 use KeyCode::*;
654
655 let mut check_channel = true;
656
657 loop {
658 if check_channel {
659 match self.indexing_receiver.try_recv() {
660 Ok((done, message)) => {
661 if !matches!(message, IndexingMessage::Error(_)) && done {
662 self.set_global_mode(GlobalMode::Default);
663 } else if let GlobalMode::Indexing { view } = &mut self.state.global_mode {
664 view.set_message(message);
665 }
666 }
667 Err(TryRecvError::Disconnected) => check_channel = false,
668 Err(TryRecvError::Empty) => {}
669 }
670 }
671
672 terminal.draw(|f| f.render_widget(&mut *self, f.area()))?;
673
674 if event::poll(Duration::from_millis(100))? {
675 let event = event::read()?;
676
677 match &mut self.state.global_mode {
678 GlobalMode::Default => {
679 if self.handle_global_event(event).await? {
680 let sync_service = self.sync_service.clone();
681 let timelines = self.timelines.clone();
682 let listen_task = self.listen_task.abort_handle();
683 let indexing_task = self.indexing_task.abort_handle();
684
685 let shutdown_task = spawn(async move {
686 sync_service.stop().await;
687
688 listen_task.abort();
689 indexing_task.abort();
690
691 for timeline in timelines.lock().values() {
692 timeline.task.abort();
693 }
694 });
695
696 self.set_global_mode(GlobalMode::Exiting { shutdown_task });
697 }
698 }
699 GlobalMode::Help => {
700 if let Event::Key(key) = event
701 && let KeyModifiers::NONE = key.modifiers
702 && let Char('q') | Esc = key.code
703 {
704 self.set_global_mode(GlobalMode::Default)
705 }
706 }
707 GlobalMode::Settings { view } => {
708 if let Event::Key(key) = event
709 && view.handle_key_press(key).await
710 {
711 self.set_global_mode(GlobalMode::Default);
712 }
713 }
714 GlobalMode::CreateRoom { view } => {
715 if let Event::Key(key) = event
716 && let KeyModifiers::NONE = key.modifiers
717 {
718 match key.code {
719 Enter => {
720 if let Some(room_name) = view.get_text() {
721 let mut request = CreateRoomRequest::new();
722 request.name = Some(room_name);
723 if let Err(err) = self
724 .sync_service
725 .room_list_service()
726 .client()
727 .create_room(request)
728 .await
729 {
730 error!("error while creating room: {err:?}");
731 }
732 }
733 self.set_global_mode(GlobalMode::Default);
734 }
735 Esc => self.set_global_mode(GlobalMode::Default),
736 _ => view.handle_key_press(key),
737 }
738 }
739 }
740 GlobalMode::Searching { view, is_global } => {
741 if let Event::Key(key) = event {
742 match key.code {
743 Enter => {
744 if let Some(query) = view.get_text() {
745 if *is_global {
746 let mut search =
747 self.client.search_messages(query, 5).build();
748
749 let mut all_results = HashMap::new();
750 loop {
751 let Ok(results) = search.next_events().await else {
752 continue;
753 };
754 let Some(results) = results else {
755 break;
756 };
757 for (room_id, event_id) in results {
758 all_results
759 .entry(room_id)
760 .or_insert_with(Vec::new)
761 .push(event_id);
762 }
763 }
764
765 view.set_results(
766 all_results
767 .into_iter()
768 .map(|(room_id, events)| {
769 (Some(room_id), events)
770 })
771 .collect(),
772 );
773 } else if let Some((query, room)) =
774 view.get_text().zip(self.room_view.room())
775 {
776 let mut room_search = room.search_messages(query, 5);
777
778 let mut all_results = Vec::new();
779 while let Some(results) =
780 room_search.next_events().await?
781 {
782 all_results.extend(results);
783 }
784 view.set_results(vec![(None, all_results)]);
785 }
786 }
787 }
788
789 Esc => self.set_global_mode(GlobalMode::Default),
790
791 Up => view.list_state.previous(),
792
793 Down => view.list_state.next(),
794
795 _ => view.handle_key_press(key),
796 }
797 }
798 }
799 GlobalMode::Indexing { .. } => {
800 if let Event::Key(key) = event
801 && let KeyModifiers::NONE = key.modifiers
802 && let Esc = key.code
803 {
804 self.indexing_task.abort();
805 self.set_global_mode(GlobalMode::Default);
806 }
807 }
808 GlobalMode::Exiting { .. } => {}
809 }
810 }
811
812 match &self.state.global_mode {
813 GlobalMode::Default
814 | GlobalMode::Help
815 | GlobalMode::CreateRoom { .. }
816 | GlobalMode::Searching { .. }
817 | GlobalMode::Indexing { .. }
818 | GlobalMode::Settings { .. } => {}
819 GlobalMode::Exiting { shutdown_task } => {
820 if shutdown_task.is_finished() {
821 break;
822 }
823 }
824 }
825
826 if self.last_tick.elapsed() >= Self::TICK_RATE {
827 self.on_tick();
828 self.last_tick = Instant::now();
829 }
830 }
831
832 Ok(())
833 }
834
835 async fn run(&mut self, terminal: Terminal<impl Backend>) -> Result<()> {
836 self.render_loop(terminal).await?;
837
838 ratatui::restore();
840 execute!(stdout(), DisableMouseCapture)?;
841
842 Ok(())
843 }
844}
845
846impl Widget for &mut App {
847 fn render(self, area: Rect, buf: &mut Buffer) {
849 let vertical =
851 Layout::vertical([Constraint::Length(2), Constraint::Min(0), Constraint::Length(1)]);
852 let [header_area, rest_area, status_area] = vertical.areas(area);
853
854 let horizontal =
857 Layout::horizontal([Constraint::Percentage(25), Constraint::Percentage(75)]);
858 let [room_list_area, room_view_area] = horizontal.areas(rest_area);
859
860 self.render_title(header_area, buf);
861 self.room_list.render(room_list_area, buf);
862 self.room_view.render(room_view_area, buf);
863 self.status.render(status_area, buf, &mut self.state);
864
865 match &mut self.state.global_mode {
866 GlobalMode::Default => {}
867 GlobalMode::Exiting { .. } => {
868 Clear.render(rest_area, buf);
869 let centered = create_centered_throbber_area(area);
870 let throbber = Throbber::default()
871 .label("Exiting")
872 .throbber_set(throbber_widgets_tui::BRAILLE_EIGHT_DOUBLE);
873 StatefulWidget::render(throbber, centered, buf, &mut self.state.throbber_state);
874 }
875 GlobalMode::Settings { view } => {
876 view.render(area, buf);
877 }
878 GlobalMode::Help => {
879 let mut help_view = HelpView::new();
880 help_view.render(area, buf);
881 }
882 GlobalMode::CreateRoom { view } => {
883 view.render(area, buf);
884 }
885 GlobalMode::Searching { view, .. } => {
886 view.render(room_view_area, buf);
887 }
888 GlobalMode::Indexing { view } => {
889 view.render(area, buf);
890 }
891 }
892 }
893}
894
895impl App {
896 fn render_title(&self, area: Rect, buf: &mut Buffer) {
898 Paragraph::new("Multiverse").bold().centered().render(area, buf);
899 }
900}
901
902async fn configure_client(cli: Cli) -> Result<Client> {
906 let Cli { server_name, session_path, proxy, dont_share_pos: _ } = cli;
907
908 let mut client_builder = Client::builder()
909 .store_config(
910 StoreConfig::new(CrossProcessLockConfig::multi_process("multiverse"))
911 .crypto_store(SqliteCryptoStore::open(session_path.join("crypto"), None).await?)
912 .state_store(SqliteStateStore::open(session_path.join("state"), None).await?)
913 .event_cache_store(
914 SqliteEventCacheStore::open(session_path.join("cache"), None).await?,
915 ),
916 )
917 .server_name_or_homeserver_url(&server_name)
918 .with_encryption_settings(EncryptionSettings {
919 auto_enable_cross_signing: true,
920 backup_download_strategy: BackupDownloadStrategy::AfterDecryptionFailure,
921 auto_enable_backups: true,
922 })
923 .with_enable_share_history_on_invite(true)
924 .with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
925 .search_index_store(SearchIndexStoreKind::UnencryptedDirectory(
926 session_path.join("indexData"),
927 ));
928
929 if let Some(proxy_url) = proxy {
930 client_builder = client_builder.proxy(proxy_url).disable_ssl_verification();
931 }
932
933 let client = client_builder.build().await?;
934
935 log_in_or_restore_session(&client, &session_path).await?;
937
938 Ok(client)
939}
940
941async fn log_in_or_restore_session(client: &Client, session_path: &Path) -> Result<()> {
942 let session_path = session_path.join("session.json");
943
944 if let Ok(serialized) = std::fs::read_to_string(&session_path) {
945 let session: MatrixSession = serde_json::from_str(&serialized)?;
946 client.restore_session(session).await?;
947 } else {
948 login_with_password(client).await?;
949
950 if let Some(session) = client.session() {
952 let AuthSession::Matrix(session) = session else {
953 panic!("unexpected OAuth 2.0 session")
954 };
955 let serialized = serde_json::to_string(&session)?;
956 std::fs::write(session_path, serialized)?;
957
958 println!("saved session");
959 }
960 }
961
962 Ok(())
963}
964
965async fn login_with_password(client: &Client) -> Result<()> {
968 println!("Logging in with username and password…");
969
970 loop {
971 print!("\nUsername: ");
972 stdout().flush().expect("Unable to write to stdout");
973 let mut username = String::new();
974 io::stdin().read_line(&mut username).expect("Unable to read user input");
975 username = username.trim().to_owned();
976
977 let password = rpassword::prompt_password("Password.")?;
978
979 match client.matrix_auth().login_username(&username, password.trim()).await {
980 Ok(_) => {
981 println!("Logged in as {username}");
982 break;
983 }
984 Err(error) => {
985 println!("Error logging in: {error}");
986 println!("Please try again\n");
987 }
988 }
989 }
990
991 Ok(())
992}