multiverse/
main.rs

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