Question: Option<tokio::sync::mutex::MutexGuard> vs std::sync::Mutex

Hi everyone !

I rework my current UI into ratatui-rs and since my app is async I store my components in Arc<tokio::sync::Mutex>. So, since terminal.draw is not sync method, I cannot do smth like this:

terminal.draw(|frame| {
    // double mutable usage of terminal anyway will not let me do this, I added this for simplicity
    if terminal.check_state(TerminalState::ModalOpened) {
        modal.lock().await.render(frame, frame.area());
    }
})?;

so currently I do this:

let modal = if terminal.check_state(TerminalState::ModalOpened) {
    Some(modal.lock().await)
} else {
    None
};

terminal.draw(|frame| {
    if let Some(mut modal) = modal {
        modal.render(frame, frame.area());
    }
})?;

I wrap my MutexGuard into Option outside of terminal.draw because the external scope is async:

let handle_render = || {
    let terminal = terminal.clone();
    let modal = modal.clone();

    tokio::spawn(async move {
        loop {
            let mut terminal = terminal.lock().await;

            if terminal.check_state(TerminalState::Exiting) {
                break;
            }

            let modal = if terminal.check_state(TerminalState::ModalOpened) {
                Some(modal.lock().await)
            } else {
                None
            };

            terminal.draw(|frame| {
                if let Some(mut modal) = modal {
                    modal.render(frame, frame.area());
                }
            })?;

            tokio::time::sleep(Duration::from_millis(10)).await;
        }

        ratatui::restore();

        Ok(())
    })
};

The full code:

#[derive(Default)]
pub struct UI2 {
    sender: Option<Sender<HandlerOutput>>,
    receiver: Option<Receiver<HandlerOutput>>,
}

impl Feature for UI2 {
    fn set_broadcast_channel(
        &mut self,
        sender: Sender<HandlerOutput>,
        receiver: Receiver<HandlerOutput>,
    ) {
        self.sender = Some(sender);
        self.receiver = Some(receiver);
    }

    fn get_tasks(&mut self) -> anyhow::Result<Vec<Task>> {
        let mut receiver = self.receiver.as_mut().ok_or(FeatureError::ReceiverNotFound)?.clone();
        let terminal = Arc::new(Mutex::new(TerminalWrapper::new()));
        let modal = Arc::new(Mutex::new(Modal::default()));

        let handle_input = || {
            let terminal = terminal.clone();
            let modal = modal.clone();

            tokio::spawn(async move {
                let mut reader = EventStream::new();

                loop {
                    let next_event = reader.next().fuse();

                    tokio::select! {
                        Some(Ok(event)) = next_event => {
                            if let Event::Key(KeyEvent { code, .. }) = event {
                                match code {
                                    KeyCode::Esc => {
                                        terminal.lock().await.set_state(TerminalState::Exiting);
                                    },
                                    _ => {}
                                }
                            }
                        },
                        Ok(input) = receiver.recv() => {
                            match input {
                                HandlerOutput::SuccessMessage(_msg, _) => {},
                                HandlerOutput::TransferCharactersList(characters) => {
                                    let data = characters
                                        .into_iter()
                                        .map(|c| vec![
                                            "Name".to_string(),
                                            c.guid.to_string(),
                                            "Guid".to_string(),
                                            c.name,
                                        ])
                                        .collect::<Vec<_>>();

                                    *modal.lock().await = Modal::default()
                                        .set_title("SELECT CHARACTER")
                                        .set_list(data);

                                    terminal.lock().await.set_state(TerminalState::ModalOpened);
                                },
                                _ => {},
                            }
                        },
                    }
                }
            })
        };

        let handle_render = || {
            let terminal = terminal.clone();
            let modal = modal.clone();

            tokio::spawn(async move {
                loop {
                    let mut terminal = terminal.lock().await;

                    if terminal.check_state(TerminalState::Exiting) {
                        break;
                    }

                    let modal = if terminal.check_state(TerminalState::ModalOpened) {
                        Some(modal.lock().await)
                    } else {
                        None
                    };

                    terminal.draw(|frame| {
                        if let Some(mut modal) = modal {
                            modal.render(frame, frame.area());
                        }
                    })?;

                    tokio::time::sleep(Duration::from_millis(10)).await;
                }

                ratatui::restore();

                Ok(())
            })
        };

        Ok(vec![
            handle_input(),
            handle_render(),
        ])
    }
}

struct TerminalWrapper {
    _terminal: DefaultTerminal,
    _state: TerminalState,
}

impl TerminalWrapper {
    pub fn new() -> Self {
        Self {
            _terminal: ratatui::init(),
            _state: TerminalState::Idle,
        }
    }

    pub fn set_state(&mut self, new_state: TerminalState) {
        self._state = new_state;
    }

    pub fn check_state(&mut self, state: TerminalState) -> bool {
        self._state == state
    }

    pub fn draw<F>(&mut self, render_callback: F) -> anyhow::Result<()>
    where
        F: FnOnce(&mut Frame),
    {
        self._terminal.draw(render_callback)?;
        Ok(())
    }
}

#[derive(PartialEq)]
enum TerminalState {
    Idle,
    ModalOpened,
    Exiting,
}

My question is: can my current approach with Option<MutexGuard> be a good alternative for std::sync::Mutex or better just to use std::sync::Mutex for my components. Or probably it is possible to change smth in the code ?

Could you please give me the advices ?

Have you seen the guidance in the Tokio doc on this?:

3 Likes

Got it. Thank you !

2 Likes