Skip to main content

mecomp_tui/state/
component.rs

1use tokio::sync::{
2    broadcast,
3    mpsc::{UnboundedReceiver, UnboundedSender},
4};
5
6use crate::termination::Interrupted;
7
8use super::action::ComponentAction;
9
10// The audio state store.
11#[derive(Debug, Clone)]
12#[allow(clippy::module_name_repetitions)]
13pub struct ComponentState {
14    state_tx: UnboundedSender<ActiveComponent>,
15}
16
17impl ComponentState {
18    /// create a new audio state store, and return the receiver for listening to state updates.
19    #[must_use]
20    pub fn new() -> (Self, UnboundedReceiver<ActiveComponent>) {
21        let (state_tx, state_rx) = tokio::sync::mpsc::unbounded_channel::<ActiveComponent>();
22
23        (Self { state_tx }, state_rx)
24    }
25
26    /// a loop that updates the active component 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<ComponentAction>,
34        mut interrupt_rx: broadcast::Receiver<Interrupted>,
35    ) -> anyhow::Result<Interrupted> {
36        let mut state = ActiveComponent::default();
37
38        // the initial state once
39        self.state_tx.send(state)?;
40
41        let result = loop {
42            tokio::select! {
43                // Handle the actions coming from the UI
44                Some(action) = action_rx.recv() => {
45                    state = Self::handle_action(state, action);
46                    self.state_tx.send(state)?;
47                },
48                // Catch and handle interrupt signal to gracefully shutdown
49                Ok(interrupted) = interrupt_rx.recv() => {
50                    break interrupted;
51                }
52            }
53        };
54
55        Ok(result)
56    }
57
58    /// Handles the action, returning the new state.
59    #[must_use]
60    const fn handle_action(state: ActiveComponent, action: ComponentAction) -> ActiveComponent {
61        match action {
62            ComponentAction::Next => state.next(),
63            ComponentAction::Previous => state.prev(),
64            ComponentAction::Set(new_state) => new_state,
65        }
66    }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
70#[allow(clippy::module_name_repetitions)]
71pub enum ActiveComponent {
72    #[default]
73    Sidebar,
74    QueueBar,
75    ControlPanel,
76    ContentView,
77}
78
79impl ActiveComponent {
80    #[must_use]
81    pub const fn next(self) -> Self {
82        match self {
83            Self::Sidebar => Self::ContentView,
84            Self::ContentView => Self::QueueBar,
85            Self::QueueBar => Self::ControlPanel,
86            Self::ControlPanel => Self::Sidebar,
87        }
88    }
89
90    #[must_use]
91    pub const fn prev(self) -> Self {
92        match self {
93            Self::Sidebar => Self::ControlPanel,
94            Self::ContentView => Self::Sidebar,
95            Self::QueueBar => Self::ContentView,
96            Self::ControlPanel => Self::QueueBar,
97        }
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use rstest::rstest;
104
105    use super::*;
106
107    #[test]
108    fn test_next() {
109        assert_eq!(
110            ActiveComponent::Sidebar.next(),
111            ActiveComponent::ContentView
112        );
113        assert_eq!(
114            ActiveComponent::ContentView.next(),
115            ActiveComponent::QueueBar
116        );
117        assert_eq!(
118            ActiveComponent::QueueBar.next(),
119            ActiveComponent::ControlPanel
120        );
121        assert_eq!(
122            ActiveComponent::ControlPanel.next(),
123            ActiveComponent::Sidebar
124        );
125    }
126
127    #[test]
128    fn test_prev() {
129        assert_eq!(
130            ActiveComponent::Sidebar.prev(),
131            ActiveComponent::ControlPanel
132        );
133        assert_eq!(
134            ActiveComponent::ContentView.prev(),
135            ActiveComponent::Sidebar
136        );
137        assert_eq!(
138            ActiveComponent::QueueBar.prev(),
139            ActiveComponent::ContentView
140        );
141        assert_eq!(
142            ActiveComponent::ControlPanel.prev(),
143            ActiveComponent::QueueBar
144        );
145    }
146
147    #[rstest]
148    #[case::next(
149        ActiveComponent::Sidebar,
150        ComponentAction::Next,
151        ActiveComponent::ContentView
152    )]
153    #[case::next(
154        ActiveComponent::ContentView,
155        ComponentAction::Next,
156        ActiveComponent::QueueBar
157    )]
158    #[case::next(
159        ActiveComponent::QueueBar,
160        ComponentAction::Next,
161        ActiveComponent::ControlPanel
162    )]
163    #[case::next(
164        ActiveComponent::ControlPanel,
165        ComponentAction::Next,
166        ActiveComponent::Sidebar
167    )]
168    #[case::prev(
169        ActiveComponent::Sidebar,
170        ComponentAction::Previous,
171        ActiveComponent::ControlPanel
172    )]
173    #[case::prev(
174        ActiveComponent::ContentView,
175        ComponentAction::Previous,
176        ActiveComponent::Sidebar
177    )]
178    #[case::prev(
179        ActiveComponent::QueueBar,
180        ComponentAction::Previous,
181        ActiveComponent::ContentView
182    )]
183    #[case::prev(
184        ActiveComponent::ControlPanel,
185        ComponentAction::Previous,
186        ActiveComponent::QueueBar
187    )]
188    #[case::set(
189        ActiveComponent::Sidebar,
190        ComponentAction::Set(ActiveComponent::ContentView),
191        ActiveComponent::ContentView
192    )]
193    #[case::set(
194        ActiveComponent::ContentView,
195        ComponentAction::Set(ActiveComponent::QueueBar),
196        ActiveComponent::QueueBar
197    )]
198    fn test_handle_action(
199        #[case] starting_state: ActiveComponent,
200        #[case] action: ComponentAction,
201        #[case] expected_state: ActiveComponent,
202    ) {
203        let new_state = ComponentState::handle_action(starting_state, action);
204
205        assert_eq!(new_state, expected_state);
206    }
207}