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