cardinal-app-core 0.1.4

Core command grammar and domain model for Cardinal.
Documentation
//! Workspace state and selection movement.
//!
//! The workspace is visual state only. Domain actions are represented as
//! parsed commands and future effect requests.

use crate::command::AgendaRange;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceState {
    pub view: View,
    pub selected_index: usize,
    pub status: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum View {
    Home,
    Inboxes,
    MailList { mailbox: String },
    MailReader { message_id: String },
    Calendars,
    Agenda { range: AgendaRange },
    EventReader { event_id: String },
    SearchResults { query: String },
    Compose,
}

impl Default for WorkspaceState {
    fn default() -> Self {
        Self {
            view: View::Home,
            selected_index: 0,
            status: None,
        }
    }
}

impl WorkspaceState {
    pub fn select_next(&mut self, len: usize) {
        if len == 0 {
            self.selected_index = 0;
            return;
        }

        self.selected_index = (self.selected_index + 1).min(len - 1);
    }

    pub fn select_previous(&mut self) {
        self.selected_index = self.selected_index.saturating_sub(1);
    }

    pub fn select_top(&mut self) {
        self.selected_index = 0;
    }

    pub fn select_bottom(&mut self, len: usize) {
        self.selected_index = len.saturating_sub(1);
    }

    pub fn set_view(&mut self, view: View) {
        self.view = view;
        self.selected_index = 0;
    }

    pub fn set_status(&mut self, status: impl Into<String>) {
        self.status = Some(status.into());
    }

    pub fn clear_status(&mut self) {
        self.status = None;
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn select_next_stops_at_end() {
        let mut state = WorkspaceState::default();
        state.select_next(2);
        state.select_next(2);
        state.select_next(2);
        assert_eq!(state.selected_index, 1);
    }

    #[test]
    fn select_previous_saturates_at_zero() {
        let mut state = WorkspaceState::default();
        state.select_previous();
        assert_eq!(state.selected_index, 0);
    }

    #[test]
    fn select_next_zero_len_resets_to_zero() {
        let mut state = WorkspaceState {
            selected_index: 4,
            ..WorkspaceState::default()
        };
        state.select_next(0);
        assert_eq!(state.selected_index, 0);
    }

    #[test]
    fn select_top_resets_selection() {
        let mut state = WorkspaceState {
            selected_index: 8,
            ..WorkspaceState::default()
        };
        state.select_top();
        assert_eq!(state.selected_index, 0);
    }

    #[test]
    fn select_bottom_tracks_list_len_and_saturates() {
        let mut state = WorkspaceState::default();
        state.select_bottom(3);
        assert_eq!(state.selected_index, 2);

        state.select_bottom(0);
        assert_eq!(state.selected_index, 0);
    }

    #[test]
    fn status_set_and_clear_round_trip() {
        let mut state = WorkspaceState::default();
        state.set_status("sync ok");
        assert_eq!(state.status, Some("sync ok".to_owned()));
        state.clear_status();
        assert_eq!(state.status, None);
    }

    #[test]
    fn set_view_resets_selection() {
        let mut state = WorkspaceState::default();
        state.select_next(10);
        state.set_view(View::Inboxes);
        assert_eq!(state.selected_index, 0);
        assert_eq!(state.view, View::Inboxes);
    }
}