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