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