Skip to main content

mecomp_tui/state/
view.rs

1//! The `ViewStore` is responsible for managing the `CurrentView` to be displayed.
2
3use tokio::sync::{
4    broadcast,
5    mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
6};
7
8use crate::{termination::Interrupted, ui::components::content_view::ActiveView};
9
10use super::action::ViewAction;
11
12/// The `ViewStore` is responsible for managing the `CurrentView` to be displayed.
13#[allow(clippy::module_name_repetitions)]
14pub struct ViewState {
15    state_tx: UnboundedSender<ActiveView>,
16}
17
18impl ViewState {
19    /// Create a new `ViewStore`.
20    #[must_use]
21    pub fn new() -> (Self, UnboundedReceiver<ActiveView>) {
22        let (state_tx, state_rx) = unbounded_channel::<ActiveView>();
23        (Self { state_tx }, state_rx)
24    }
25
26    /// A loop that updates the store when requested
27    ///
28    /// # Errors
29    ///
30    /// Fails if the state cannot be sent
31    pub async fn main_loop(
32        &self,
33        mut action_rx: UnboundedReceiver<ViewAction>,
34        mut interrupt_rx: broadcast::Receiver<Interrupted>,
35    ) -> anyhow::Result<Interrupted> {
36        let mut state = ActiveView::default();
37        // a stack to keep track of previous views
38        let mut view_history = vec![state.clone()];
39        let mut view_index = 0;
40
41        // the initial state once
42        self.state_tx.send(state.clone())?;
43
44        let result = loop {
45            tokio::select! {
46                // Handle the actions coming from the UI
47                // and process them to do async operations
48                Some(action) = action_rx.recv() => {
49                    state = self.handle_action(&mut view_history, &mut view_index, action);
50                    self.state_tx.send(state.clone())?;
51                },
52                // Catch and handle interrupt signal to gracefully shutdown
53                Ok(interrupted) = interrupt_rx.recv() => {
54                    break interrupted;
55                }
56            }
57        };
58
59        Ok(result)
60    }
61
62    /// Handle the action, returning the new state
63    pub fn handle_action(
64        &self,
65        view_history: &mut Vec<ActiveView>,
66        view_index: &mut usize,
67        action: ViewAction,
68    ) -> ActiveView {
69        match action {
70            ViewAction::Set(view) => {
71                view_history.truncate(*view_index + 1);
72                view_history.push(view);
73                *view_index = view_history.len() - 1;
74            }
75            ViewAction::Back if *view_index > 0 => *view_index -= 1,
76            ViewAction::Next if *view_index < view_history.len() - 1 => *view_index += 1,
77            _ => {}
78        }
79        view_history.get(*view_index).cloned().unwrap_or_default()
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use pretty_assertions::assert_eq;
87
88    #[test]
89    fn test_handle_action() {
90        let (view, _) = ViewState::new();
91
92        let mut view_history = vec![ActiveView::default()];
93        let mut view_index = 0;
94
95        let mut state = view.handle_action(
96            &mut view_history,
97            &mut view_index,
98            ViewAction::Set(ActiveView::Search),
99        );
100        assert_eq!(state, ActiveView::Search);
101
102        state = view.handle_action(
103            &mut view_history,
104            &mut view_index,
105            ViewAction::Set(ActiveView::Songs),
106        );
107        assert_eq!(state, ActiveView::Songs);
108
109        state = view.handle_action(
110            &mut view_history,
111            &mut view_index,
112            ViewAction::Set(ActiveView::Artists),
113        );
114        assert_eq!(state, ActiveView::Artists);
115
116        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
117        assert_eq!(state, ActiveView::Songs);
118
119        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
120        assert_eq!(state, ActiveView::Search);
121
122        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
123        assert_eq!(state, ActiveView::default());
124
125        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
126        assert_eq!(state, ActiveView::default());
127    }
128
129    #[test]
130    fn test_forward_backward() {
131        let (view, _) = ViewState::new();
132
133        let mut view_history = vec![ActiveView::default()];
134        let mut view_index = 0;
135
136        let mut state = view.handle_action(
137            &mut view_history,
138            &mut view_index,
139            ViewAction::Set(ActiveView::Search),
140        );
141        assert_eq!(state, ActiveView::Search);
142
143        state = view.handle_action(
144            &mut view_history,
145            &mut view_index,
146            ViewAction::Set(ActiveView::Songs),
147        );
148        assert_eq!(state, ActiveView::Songs);
149
150        state = view.handle_action(
151            &mut view_history,
152            &mut view_index,
153            ViewAction::Set(ActiveView::Artists),
154        );
155        assert_eq!(state, ActiveView::Artists);
156
157        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
158        assert_eq!(state, ActiveView::Songs);
159
160        state = view.handle_action(
161            &mut view_history,
162            &mut view_index,
163            ViewAction::Set(ActiveView::Albums),
164        );
165        assert_eq!(state, ActiveView::Albums);
166
167        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
168        assert_eq!(state, ActiveView::Songs);
169
170        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Next);
171        assert_eq!(state, ActiveView::Albums);
172
173        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Next);
174        assert_eq!(state, ActiveView::Albums);
175
176        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
177        assert_eq!(state, ActiveView::Songs);
178
179        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
180        assert_eq!(state, ActiveView::Search);
181
182        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
183        assert_eq!(state, ActiveView::default());
184
185        state = view.handle_action(&mut view_history, &mut view_index, ViewAction::Back);
186        assert_eq!(state, ActiveView::default());
187    }
188}