multiverse/widgets/
status.rs

1use std::{
2    sync::{
3        mpsc::{self, Receiver},
4        Arc,
5    },
6    time::Duration,
7};
8
9use matrix_sdk_common::locks::Mutex;
10use ratatui::{
11    prelude::{Buffer, Rect, *},
12    widgets::Paragraph,
13};
14use tokio::{
15    spawn,
16    task::{spawn_blocking, JoinHandle},
17    time::sleep,
18};
19
20use crate::{AppState, GlobalMode};
21
22const MESSAGE_DURATION: Duration = Duration::from_secs(4);
23
24pub struct Status {
25    /// Content of the latest status message, if set.
26    last_status_message: Arc<Mutex<Option<String>>>,
27
28    /// An [mpsc::Sender] that other widgets can use to change the status
29    /// message.
30    message_sender: mpsc::Sender<String>,
31
32    /// The task listening for status messages to be received over the
33    /// [mpsc::Receiver].
34    _receiver_task: JoinHandle<()>,
35}
36
37impl Default for Status {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43/// A handle to the [`Status`] widget, this handle can be moved to different
44/// threads where it can be used to set the status message.
45#[derive(Clone)]
46pub struct StatusHandle {
47    message_sender: mpsc::Sender<String>,
48}
49
50impl StatusHandle {
51    /// Set the current status message (displayed at the bottom), for a few
52    /// seconds.
53    pub fn set_message(&self, status: String) {
54        self.message_sender.send(status).expect(
55            "We should be able to send the status message since the receiver is alive \
56                  as long as we are alive",
57        );
58    }
59}
60
61impl Status {
62    /// Create a new empty [`Status`] widget.
63    pub fn new() -> Self {
64        let (message_sender, receiver) = mpsc::channel();
65        let last_status_message = Arc::new(Mutex::new(None));
66
67        let receiver_task = spawn_blocking({
68            let last_status_message = last_status_message.clone();
69            move || Self::receiving_task(receiver, last_status_message)
70        });
71
72        Self { last_status_message, _receiver_task: receiver_task, message_sender }
73    }
74
75    fn receiving_task(receiver: Receiver<String>, status_message: Arc<Mutex<Option<String>>>) {
76        let mut clear_message_task: Option<JoinHandle<()>> = None;
77
78        while let Ok(message) = receiver.recv() {
79            if let Some(task) = clear_message_task.take() {
80                task.abort();
81            }
82
83            {
84                let mut status_message = status_message.lock();
85                *status_message = Some(message);
86            }
87
88            clear_message_task = Some(spawn({
89                let status_message = status_message.clone();
90
91                async move {
92                    // Clear the status message in 4 seconds.
93                    sleep(MESSAGE_DURATION).await;
94                    status_message.lock().take();
95                }
96            }));
97        }
98    }
99
100    /// Set the current status message (displayed at the bottom), for a few
101    /// seconds.
102    pub fn set_message(&self, status: String) {
103        self.message_sender.send(status).expect(
104            "We should be able to send the status message since the receiver is alive \
105                  as long as we are alive",
106        );
107    }
108
109    /// Get a handle to the [`Status`] widget, this can be used to set the
110    /// status message from a separate thread.
111    pub fn handle(&self) -> StatusHandle {
112        StatusHandle { message_sender: self.message_sender.clone() }
113    }
114}
115
116impl StatefulWidget for &mut Status {
117    type State = AppState;
118
119    /// Render the bottom part of the screen, with a status message if one is
120    /// set, or a default help message otherwise.
121    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
122        let status_message = self.last_status_message.lock();
123
124        let content = if let Some(status_message) = status_message.as_deref() {
125            status_message
126        } else {
127            let AppState { global_mode, throbber_state: _ } = state;
128
129            match global_mode {
130                GlobalMode::Help => "Press q to exit the help screen",
131                GlobalMode::Settings { .. } => "Press ESC to exit the settings screen",
132                GlobalMode::Default => "Press F1 to show the help screen",
133                GlobalMode::Exiting { .. } => "",
134            }
135        };
136
137        Paragraph::new(content).centered().render(area, buf);
138    }
139}