Skip to main content

multiverse/
main.rs

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    /// The homeserver the client should connect to.
80    server_name: String,
81
82    /// The path where session specific data should be stored.
83    #[clap(default_value = "/tmp/")]
84    session_path: PathBuf,
85
86    /// Set the proxy that should be used for the connection.
87    #[clap(short, long, env = "PROXY")]
88    proxy: Option<Url>,
89
90    /// Whether to _not_ reload the `pos`ition sliding sync token from disk at
91    /// start or not, for the room list sliding sync.
92    ///
93    /// Set to false by default (i.e. reload the position from disk).
94    #[clap(short, long, default_value_t = false)]
95    dont_share_pos: bool,
96}
97
98#[derive(Default)]
99pub enum GlobalMode {
100    /// The default mode, no popout screen is opened.
101    #[default]
102    Default,
103    /// Mode where we have opened the help screen.
104    Help,
105    /// Mode where we have opened the settings screen.
106    Settings { view: SettingsView },
107    /// Mode where we are shutting our tasks down and exiting multiverse.
108    Exiting { shutdown_task: JoinHandle<()> },
109    /// Mode where we have opened the create room screen
110    CreateRoom { view: CreateRoomView },
111    /// Mode where we have opened the search screen
112    Searching { view: SearchingView, is_global: bool },
113    /// Mode where we have opened the indexing screen
114    Indexing { view: IndexingView },
115}
116
117/// Helper function to create a centered rect using up certain percentage of the
118/// available rect `r`
119fn 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    // Watch for errors in background jobs.
144    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    /// What popup are we showing that is covering the majority of the screen,
170    /// mainly used for help and settings screens.
171    global_mode: GlobalMode,
172
173    /// State for a global throbber.
174    throbber_state: ThrobberState,
175}
176
177struct App {
178    /// Reference to the main SDK client.
179    client: Client,
180
181    /// The sync service used for synchronizing events.
182    sync_service: Arc<SyncService>,
183
184    /// Timelines data structures for each room.
185    timelines: Timelines,
186
187    /// The room list widget on the left-hand side of the screen.
188    room_list: RoomList,
189
190    /// A view displaying the contents of the selected room, the widget on the
191    /// right-hand side of the screen.
192    room_view: RoomView,
193
194    /// Task listening to room list service changes, and spawning timelines.
195    listen_task: JoinHandle<()>,
196
197    /// Task that is indexing events for search.
198    indexing_task: JoinHandle<()>,
199
200    /// Receiver that notifies when indexing is complete.
201    indexing_receiver: Receiver<(bool, IndexingMessage)>,
202
203    /// The status widget at the bottom of the screen.
204    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        // This will sync (with encryption) until an error happens or the
233        // program is stopped.
234        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                // Apply the diffs to the list of room entries.
282                let mut rooms = rooms.lock();
283
284                for diff in diffs {
285                    diff.apply(&mut rooms);
286                }
287
288                // Collect rooms early to release the room entries list lock.
289                (*rooms).clone()
290            };
291
292            let mut new_rooms = HashMap::new();
293            let mut new_timelines = Vec::new();
294
295            // Update all the room info for all rooms.
296            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            // Initialize all the new rooms.
314            for room in
315                all_rooms.into_iter().filter(|room| !previous_rooms.contains(room.room_id()))
316            {
317                // Initialize the timeline.
318                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                // Save the timeline in the cache.
329                let (items, stream) = timeline.subscribe().await;
330                let items = Arc::new(Mutex::new(items));
331
332                // Spawn a timeline task that will listen to all the timeline
333                // item changes.
334                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                // Save the room list service room in the cache.
353                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        // Spin until there are rooms to index
369        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    /// The sender sends (progress, done?, error?).
511    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        // First index everything in the cache
535        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        // Now index from the server
549        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        // At this point the user has exited the loop, so shut down the
834        // application.
835        ratatui::restore();
836        execute!(stdout(), DisableMouseCapture)?;
837
838        Ok(())
839    }
840}
841
842impl Widget for &mut App {
843    /// Render the whole app.
844    fn render(self, area: Rect, buf: &mut Buffer) {
845        // Create a space for header, room list and timeline and the footer.
846        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        // Create two chunks with equal horizontal screen space. One for the
851        // list and the other for the info block.
852        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    /// Render the top square (title of the program).
893    fn render_title(&self, area: Rect, buf: &mut Buffer) {
894        Paragraph::new("Multiverse").bold().centered().render(area, buf);
895    }
896}
897
898/// Configure the client so it's ready for sync'ing.
899///
900/// Will log in or reuse a previous session.
901async 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    // Try reading a session, otherwise create a new one.
933    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        // Immediately save the session to disk.
948        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
962/// Asks the user of a username and password, and try to login using the matrix
963/// auth with those.
964async 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}