megane 0.0.8

AWS Cloudwatch cli viewer
use std::collections::BTreeMap;

use anyhow::{anyhow, Result};
use async_trait::async_trait;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use tui::{
    backend::Backend,
    layout::{Constraint, Direction, Layout, Rect},
    Frame,
};

use crate::{
    event::LogEventEvent,
    key_event_wrapper::KeyEventWrapper,
    state::{
        logevents_state::LogEventsState,
        search_state::{SearchMode, SearchState},
    },
    ui::{event_area::EventArea, help::Help, side_menu::SideMenu, status_bar::StatusBar, Drawable},
    utils::key_maps_stringify,
};

/// which component selected
#[derive(PartialEq, Debug)]
pub enum SelectState {
    SideMenu,
    EventAreas(usize),
}

pub struct App<B>
where
    B: Backend,
{
    side_menu: SideMenu<B>,
    event_areas: Vec<EventArea<B>>,
    logevent_states: [Arc<Mutex<LogEventsState>>; 4],
    logevent_inst_txs: [mpsc::Sender<LogEventEvent>; 4],
    status_bar: StatusBar<B>,
    select_state: SelectState,
    show_help: bool,
    fold: bool,
    help: Help<B>,
    free_idx: [bool; 4],
}

impl<B> App<B>
where
    B: Backend,
{
    pub async fn new(
        side_menu: SideMenu<B>,
        event_areas: Vec<EventArea<B>>,
        logevent_states: [Arc<Mutex<LogEventsState>>; 4],
        logevent_inst_txs: [mpsc::Sender<LogEventEvent>; 4],
        status_bar: StatusBar<B>,
        show_help: bool,
        fold: bool,
    ) -> Self {
        App {
            side_menu,
            event_areas,
            logevent_states,
            logevent_inst_txs,
            status_bar,
            select_state: SelectState::SideMenu,
            show_help,
            fold,
            help: Help::new(),
            free_idx: [true, true, true, true],
        }
    }

    pub fn split_event_area(&self, rect: Rect) -> Vec<Rect> {
        let constaints = [Constraint::Percentage(50), Constraint::Percentage(50)];
        let base_chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints(constaints.as_ref())
            .split(rect);
        let left_chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints(constaints.as_ref())
            .split(base_chunks[0]);
        let right_chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints(constaints.as_ref())
            .split(base_chunks[1]);

        match self.event_areas.len() {
            1 => {
                vec![left_chunks[0]
                    .union(left_chunks[1])
                    .union(right_chunks[0])
                    .union(right_chunks[1])]
            }
            2 => {
                vec![
                    left_chunks[0].union(left_chunks[1]),
                    right_chunks[0].union(right_chunks[1]),
                ]
            }
            3 => {
                vec![
                    left_chunks[0],
                    right_chunks[0].union(right_chunks[1]),
                    left_chunks[1],
                ]
            }
            4 => {
                vec![
                    left_chunks[0],
                    right_chunks[0],
                    left_chunks[1],
                    right_chunks[1],
                ]
            }
            _ => vec![],
        }
    }

    pub fn toggle_side_fold(&mut self) {
        self.fold = !self.fold;
    }

    pub fn toggle_show_help(&mut self, is_show: bool) {
        self.show_help = is_show;
    }

    pub fn rotate_select_state(&mut self, key: KeyCode) {
        let event_areas_len = self.event_areas.len();
        match self.select_state {
            SelectState::SideMenu => {
                if let KeyCode::Right = key {
                    if event_areas_len > 0 {
                        self.select_state = SelectState::EventAreas(0);
                    }
                }
            }
            SelectState::EventAreas(idx) => match key {
                KeyCode::Left => match idx {
                    0 => self.select_state = SelectState::SideMenu,
                    1 => self.select_state = SelectState::EventAreas(0),
                    2 => self.select_state = SelectState::SideMenu,
                    3 => self.select_state = SelectState::EventAreas(2),
                    _ => {}
                },
                KeyCode::Right => match idx {
                    0 => {
                        if event_areas_len > 1 {
                            self.select_state = SelectState::EventAreas(1);
                        }
                    }
                    2 => {
                        if event_areas_len > 3 {
                            self.select_state = SelectState::EventAreas(3);
                        } else {
                            self.select_state = SelectState::EventAreas(1);
                        }
                    }
                    _ => {}
                },
                KeyCode::Down => match idx {
                    0 => {
                        if event_areas_len > 2 {
                            self.select_state = SelectState::EventAreas(2);
                        }
                    }
                    1 => {
                        if event_areas_len > 3 {
                            self.select_state = SelectState::EventAreas(3);
                        }
                    }
                    _ => {}
                },
                KeyCode::Up => match idx {
                    2 => self.select_state = SelectState::EventAreas(0),
                    3 => self.select_state = SelectState::EventAreas(1),
                    _ => {}
                },
                _ => {}
            },
        }
    }

    /// get index to push the next event_area
    pub fn get_next_idx(&self) -> Result<usize> {
        for (idx, is_free) in self.free_idx.iter().enumerate() {
            if *is_free {
                return Ok(idx);
            }
        }
        Err(anyhow!("Free idx does not exist. Something wrong."))
    }
}

impl<B> Default for App<B>
where
    B: Backend,
{
    fn default() -> Self {
        // dummy channel
        let (tx1, _rx1): (mpsc::Sender<LogEventEvent>, mpsc::Receiver<LogEventEvent>) =
            mpsc::channel(1);
        let (tx2, _rx2): (mpsc::Sender<LogEventEvent>, mpsc::Receiver<LogEventEvent>) =
            mpsc::channel(1);
        let (tx3, _rx3): (mpsc::Sender<LogEventEvent>, mpsc::Receiver<LogEventEvent>) =
            mpsc::channel(1);
        let (tx4, _rx4): (mpsc::Sender<LogEventEvent>, mpsc::Receiver<LogEventEvent>) =
            mpsc::channel(1);
        App {
            side_menu: SideMenu::default(),
            event_areas: vec![],
            logevent_states: [
                Arc::new(Mutex::new(LogEventsState::default())),
                Arc::new(Mutex::new(LogEventsState::default())),
                Arc::new(Mutex::new(LogEventsState::default())),
                Arc::new(Mutex::new(LogEventsState::default())),
            ],
            logevent_inst_txs: [tx1, tx2, tx3, tx4],
            status_bar: StatusBar::default(),
            select_state: SelectState::SideMenu,
            show_help: false,
            fold: false,
            help: Help::default(),
            free_idx: [true, true, true, true],
        }
    }
}

#[async_trait]
impl<B> Drawable<B> for App<B>
where
    B: Backend + Send,
{
    fn draw(&mut self, f: &mut Frame<'_, B>, _area: Rect) {
        let (left, right) = if self.fold { (3, 97) } else { (30, 70) };
        // base_chunks[0] - side menu and event area
        // base_chunks[1] - status bar area
        let base_chunks = Layout::default()
            .direction(Direction::Vertical)
            .constraints([Constraint::Max(100), Constraint::Length(2)].as_ref())
            .split(f.size());
        // chunks[0] - side menu
        // chunks[1] - event area
        let chunks = Layout::default()
            .direction(Direction::Horizontal)
            .constraints([Constraint::Percentage(left), Constraint::Percentage(right)].as_ref())
            .split(base_chunks[0]);
        // update select state
        self.side_menu.set_select(match self.select_state {
            SelectState::SideMenu => true,
            SelectState::EventAreas(_) => false,
        });
        for (i, v) in self.event_areas.iter_mut().enumerate() {
            v.set_select(match self.select_state {
                SelectState::SideMenu => false,
                SelectState::EventAreas(idx) => i == idx,
            })
        }
        if self.show_help {
            let chunk = Layout::default()
                .constraints([Constraint::Percentage(100)])
                .split(f.size());
            self.help.draw(f, chunk[0]);
        } else {
            // draw side menu and event areas
            self.side_menu.draw(f, chunks[0]);
            let event_area_rects = self.split_event_area(chunks[1]);
            for (i, v) in self.event_areas.iter_mut().enumerate() {
                v.draw(f, event_area_rects[i]);
            }
            let mut maps: BTreeMap<KeyEventWrapper, String> = BTreeMap::new();
            self.push_key_maps(&mut maps);
            let maps_str = key_maps_stringify(&maps);
            self.status_bar.update_text(&maps_str);
            self.status_bar.draw(f, base_chunks[1]);
        }
    }

    async fn handle_event(&mut self, event: KeyEvent) -> bool {
        if let KeyCode::Char('q') = event.code {
            self.toggle_show_help(false);
        } else {
            let solved = match self.select_state {
                SelectState::SideMenu => self.side_menu.handle_event(event).await,
                SelectState::EventAreas(idx) => {
                    if let Some(event_area) = self.event_areas.get_mut(idx) {
                        event_area.handle_event(event).await
                    } else {
                        false
                    }
                }
            };
            if !solved {
                match event.code {
                    KeyCode::Char('?') => {
                        self.toggle_show_help(true);
                    }
                    KeyCode::Tab => {
                        self.toggle_side_fold();
                    }
                    KeyCode::Right => {
                        self.rotate_select_state(KeyCode::Right);
                    }
                    KeyCode::Up => {
                        self.rotate_select_state(KeyCode::Up);
                    }
                    KeyCode::Left => {
                        self.rotate_select_state(KeyCode::Left);
                    }
                    KeyCode::Down => {
                        self.rotate_select_state(KeyCode::Down);
                    }
                    KeyCode::Enter => {
                        if let SelectState::SideMenu = self.select_state {
                            // log group selection updated
                            let current_log_groups = self
                                .event_areas
                                .iter()
                                .map(|i| i.log_group_name())
                                .collect::<Vec<&str>>();
                            let log_groups_to_create = self
                                .side_menu
                                .selected_log_groups()
                                .iter()
                                .filter(|group| !current_log_groups.contains(&group.as_str()))
                                .collect::<Vec<&String>>();
                            let mut idx_to_remove = vec![];
                            current_log_groups
                                .iter()
                                .enumerate()
                                .for_each(|(i, group)| {
                                    if !self
                                        .side_menu
                                        .selected_log_groups()
                                        .contains(&group.to_string())
                                    {
                                        idx_to_remove.push(i);
                                    }
                                });
                            for i in idx_to_remove {
                                if self.event_areas.len() > i {
                                    self.event_areas.remove(i);
                                    self.logevent_states[i].lock().unwrap().reset();
                                    self.free_idx[i] = true;
                                }
                            }
                            for i in log_groups_to_create {
                                let idx = self.get_next_idx().unwrap();
                                self.free_idx[idx] = false;
                                let state = Arc::clone(&self.logevent_states[idx]);
                                self.event_areas.push(EventArea::new(
                                    i,
                                    state,
                                    mpsc::Sender::clone(&self.logevent_inst_txs[idx]),
                                ));
                                let _ = self.logevent_inst_txs[idx]
                                    .send(LogEventEvent::FetchLogEvents(
                                        i.to_string(),
                                        None,
                                        Some(SearchState::new(String::default(), SearchMode::Tail)),
                                        true,
                                    ))
                                    .await;
                                log::info!("A new log group added, sended an event below to LogEventEventHandler thread.\n{:?}",
                                           LogEventEvent::FetchLogEvents(
                                               i.to_string(),
                                               None,
                                               Some(SearchState::new(String::default(), SearchMode::Tail)),
                                               true,
                                           )
                                );
                            }
                        }
                    }
                    _ => {}
                }
            }
        }
        true
    }

    fn push_key_maps<'a>(
        &self,
        maps: &'a mut BTreeMap<KeyEventWrapper, String>,
    ) -> &'a mut BTreeMap<KeyEventWrapper, String> {
        maps.insert(
            KeyEventWrapper::new(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)),
            "Help".to_string(),
        );
        maps.insert(
            KeyEventWrapper::new(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)),
            "Toggle side menu".to_string(),
        );
        maps.insert(
            KeyEventWrapper::new(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE)),
            "Move".to_string(),
        );
        maps.insert(
            KeyEventWrapper::new(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)),
            "Move".to_string(),
        );
        maps.insert(
            KeyEventWrapper::new(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)),
            "Move".to_string(),
        );
        maps.insert(
            KeyEventWrapper::new(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)),
            "Move".to_string(),
        );
        maps.insert(
            KeyEventWrapper::new(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
            "Exit".to_string(),
        );
        match self.select_state {
            SelectState::SideMenu => {
                self.side_menu.push_key_maps(maps);
            }
            SelectState::EventAreas(idx) => {
                if let Some(event_area) = self.event_areas.get(idx) {
                    event_area.push_key_maps(maps);
                }
            }
        }
        maps
    }
}

#[cfg(test)]
mod tests {
    use crossterm::event::{KeyCode, KeyModifiers};
    use tui::{backend::TestBackend, buffer::Buffer, layout::Rect, style::Color};

    use super::*;
    use crate::{loggroups::LogGroups, state::loggroups_state::LogGroupsState, test_helper::*};

    fn test_case(
        app: &mut App<TestBackend>,
        side_menu_color: Color,
        event_area_color: Color,
        lines: Vec<&str>,
        side_menu_length: u16,
        header_exists: bool,
    ) {
        let mut terminal = get_test_terminal(100, 10);
        let lines = if !lines.is_empty() {
            lines
        } else {
            vec![
                "┌Log Groups [type to search]─┐                                                                      ",
                "│                            │                                                                      ",
                "│                            │                                                                      ",
                "│                            │                                                                      ",
                "│                            │                                                                      ",
                "│                            │                                                                      ",
                "│                            │                                                                      ",
                "└────────────────────────────┘                                                                      ",
                "         [?]Help/[BackSpace]Incremental filtering (remove)/[C+Ctrl]Exit/[Chars]Incremental filtering",
                "   (add)/[ENTER]Select log group/[TAB]Toggle side menu/[←]Move/[↑]Prev log group/[→]Move/[↓]Next log",
            ]
        };
        let mut expected = Buffer::with_lines(lines);
        for y in 0..10 {
            for x in 0..100 {
                let ch = expected.get_mut(x, y);
                if y == 0 {
                    if x < side_menu_length {
                        ch.set_fg(side_menu_color);
                    }
                } else if y == 7 {
                    if x >= side_menu_length {
                        if ch.symbol != " " {
                            ch.set_fg(event_area_color);
                        }
                    } else {
                        ch.set_fg(side_menu_color);
                    }
                } else if y == 2 {
                    if ch.symbol == "" {
                        if x >= side_menu_length {
                            ch.set_fg(event_area_color);
                        } else {
                            ch.set_fg(side_menu_color);
                        }
                    } else if x >= side_menu_length && header_exists {
                        ch.set_fg(Color::White);
                    }
                } else if y == 8 || y == 9 {
                    ch.set_fg(Color::Reset);
                } else if ch.symbol != " " {
                    if x >= side_menu_length {
                        ch.set_fg(event_area_color);
                    } else {
                        ch.set_fg(side_menu_color);
                    }
                }
            }
        }
        terminal
            .draw(|f| {
                app.draw(f, f.size());
            })
            .unwrap();
        terminal.backend().assert_buffer(&expected);
    }

    #[test]
    fn test_split_event_area() {
        let mut app: App<TestBackend> = App::default();
        // no event areas
        let result = app.split_event_area(Rect::new(0, 0, 100, 100));
        let expect: Vec<Rect> = vec![];
        assert_eq!(expect, result);
        // 1 event area
        app.event_areas.push(EventArea::default());
        let result = app.split_event_area(Rect::new(0, 0, 100, 100));
        let expect = vec![Rect::new(0, 0, 100, 100)];
        assert_eq!(expect, result);
        // 2 event areas
        app.event_areas.push(EventArea::default());
        let result = app.split_event_area(Rect::new(0, 0, 100, 100));
        let expect = vec![Rect::new(0, 0, 50, 100), Rect::new(50, 0, 50, 100)];
        assert_eq!(expect, result);
        // 3 event areas
        app.event_areas.push(EventArea::default());
        let result = app.split_event_area(Rect::new(0, 0, 100, 100));
        let expect = vec![
            Rect::new(0, 0, 50, 50),
            Rect::new(50, 0, 50, 100),
            Rect::new(0, 50, 50, 50),
        ];
        assert_eq!(expect, result);
        // 4 event areas
        app.event_areas.push(EventArea::default());
        let result = app.split_event_area(Rect::new(0, 0, 100, 100));
        let expect = vec![
            Rect::new(0, 0, 50, 50),
            Rect::new(50, 0, 50, 50),
            Rect::new(0, 50, 50, 50),
            Rect::new(50, 50, 50, 50),
        ];
        assert_eq!(expect, result);
    }

    #[tokio::test]
    async fn test_draw() {
        let mut app: App<TestBackend> = App::default();
        test_case(&mut app, Color::Yellow, Color::White, vec![], 30, false);
        app.event_areas.push(EventArea::default());
        let lines = vec![
            "┌Log Groups [type to search]─┐query: [], mode: [1 minute]                                           ",
            "│                            │┌Events──────────────────────────────────────────────────────────────┐",
            "│                            ││   Timestamp     Event                                              │",
            "│                            ││                                                                    │",
            "│                            ││                                                                    │",
            "│                            ││                                                                    │",
            "│                            ││                                                                    │",
            "└────────────────────────────┘└────────────────────────────────────────────────────────────────────┘",
            "         [?]Help/[BackSpace]Incremental filtering (remove)/[C+Ctrl]Exit/[Chars]Incremental filtering",
            "   (add)/[ENTER]Select log group/[TAB]Toggle side menu/[←]Move/[↑]Prev log group/[→]Move/[↓]Next log",

        ];
        test_case(&mut app, Color::Yellow, Color::White, lines, 30, true);
        // folding side menu
        app.toggle_side_fold();
        let lines = vec![
            "┌L┐query: [], mode: [1 minute]                                                                      ",
            "│ │┌Events─────────────────────────────────────────────────────────────────────────────────────────┐",
            "│ ││   Timestamp           Event                                                                   │",
            "│ ││                                                                                               │",
            "│ ││                                                                                               │",
            "│ ││                                                                                               │",
            "│ ││                                                                                               │",
            "└─┘└───────────────────────────────────────────────────────────────────────────────────────────────┘",
            "         [?]Help/[BackSpace]Incremental filtering (remove)/[C+Ctrl]Exit/[Chars]Incremental filtering",
            "   (add)/[ENTER]Select log group/[TAB]Toggle side menu/[←]Move/[↑]Prev log group/[→]Move/[↓]Next log",
        ];
        test_case(&mut app, Color::Yellow, Color::White, lines, 3, true);
        // event area selected
        app.toggle_side_fold();
        app.select_state = SelectState::EventAreas(0);
        let lines = vec![
            "┌Log Groups [type to search]─┐query: [], mode: [1 minute]                                           ",
            "│                            │┌Events──────────────────────────────────────────────────────────────┐",
            "│                            ││   Timestamp     Event                                              │",
            "│                            ││                                                                    │",
            "│                            ││                                                                    │",
            "│                            ││                                                                    │",
            "│                            ││                                                                    │",
            "└────────────────────────────┘└────────────────────────────────────────────────────────────────────┘",
            "      [?]Help/[C+Ctrl]Exit/[ENTER]Copy to clipboard/[J]Next log event/[K]Prev log event/[S+Ctrl]Open",
            "                            search dialog/[TAB]Toggle log event open/[←]Move/[↑]Move/[→]Move/[↓]Move",


        ];
        test_case(&mut app, Color::White, Color::Yellow, lines, 30, true);
        // help dialog
        app.toggle_show_help(true);
        let lines = vec![
            "┌HELP──────────────────────────────────────────────────────────────────────────────────────────────┐",
            "│                                                                                                  │",
            "│                                                                                                  │",
            "│                                                                                                  │",
            "│                                                                                                  │",
            "│                                                                                                  │",
            "│                                                                                                  │",
            "│                                                                                                  │",
            "│                                                                                                  │",
            "└──────────────────────────────────────────────────────────────────────────────────────────────────┘",
        ];
        test_case(&mut app, Color::Reset, Color::Reset, lines, 30, false);
    }

    #[tokio::test]
    async fn test_handle_event_basis() {
        let mut app: App<TestBackend> = App::default();
        app.event_areas.push(EventArea::default());
        assert!(!app.fold);
        assert!(
            app.handle_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE))
                .await
        );
        assert!(app.fold);
        app.select_state = SelectState::EventAreas(0);
        assert!(
            app.handle_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE))
                .await
        );
        assert!(!app.fold);
        app.event_areas.pop();
        assert!(
            app.handle_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE))
                .await
        );
        assert!(
            app.handle_event(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE))
                .await
        );
        assert!(app.show_help);
        app.event_areas.push(EventArea::default());
        app.select_state = SelectState::SideMenu;
        assert!(
            app.handle_event(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE))
                .await
        );
        assert_eq!(SelectState::EventAreas(0), app.select_state);
    }

    #[tokio::test]
    async fn test_handler_event_update_eventareas() {
        // setup
        let loggroup_state = Arc::new(Mutex::new(LogGroupsState::new()));
        let mut app: App<TestBackend> = App {
            side_menu: SideMenu::new(Arc::clone(&loggroup_state)),
            ..Default::default()
        };
        // 3 log groups
        {
            let mut m_guard = loggroup_state.lock().unwrap();
            m_guard.log_groups = LogGroups::new(get_log_groups(0, 2, false));
            m_guard.get_list_items("", &[]);
        }
        // current cursor is the first log group
        app.select_state = SelectState::SideMenu;
        app.side_menu.set_select(true);
        app.side_menu
            .handle_event(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE))
            .await;

        // check the curernt state
        assert_eq!(0, app.event_areas.len());
        assert_eq!([true, true, true, true], app.free_idx);
        // fire the Enter event
        assert!(
            app.handle_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
                .await
        );
        assert_eq!(1, app.event_areas.len());
        assert_eq!([false, true, true, true], app.free_idx);
    }

    #[test]
    fn test_rotate_state() {
        let mut app: App<TestBackend> = App::default();
        app.event_areas.push(EventArea::default());
        app.event_areas.push(EventArea::default());
        app.event_areas.push(EventArea::default());
        app.event_areas.push(EventArea::default());
        // KeyCode::Left
        // EventArea(left-top) -> SideMenu
        app.select_state = SelectState::EventAreas(0);
        app.rotate_select_state(KeyCode::Left);
        assert_eq!(SelectState::SideMenu, app.select_state);
        // EventArea(right-top) -> EventArea(left-top)
        app.select_state = SelectState::EventAreas(1);
        app.rotate_select_state(KeyCode::Left);
        assert_eq!(SelectState::EventAreas(0), app.select_state);
        // EventArea(left-bottom) -> SideMenu
        app.select_state = SelectState::EventAreas(2);
        app.rotate_select_state(KeyCode::Left);
        assert_eq!(SelectState::SideMenu, app.select_state);
        // EventArea(right-bottom) -> EventArea(left-bottom)
        app.select_state = SelectState::EventAreas(3);
        app.rotate_select_state(KeyCode::Left);
        assert_eq!(SelectState::EventAreas(2), app.select_state);
        // KeyCode::Right
        // SideMenu -> EventArea(left-top)
        app.select_state = SelectState::SideMenu;
        app.rotate_select_state(KeyCode::Right);
        assert_eq!(SelectState::EventAreas(0), app.select_state);
        // EventArea(left-top) -> EventArea(right-top)
        app.select_state = SelectState::EventAreas(0);
        app.rotate_select_state(KeyCode::Right);
        assert_eq!(SelectState::EventAreas(1), app.select_state);
        // EventArea(right-top) -> no change
        app.select_state = SelectState::EventAreas(1);
        app.rotate_select_state(KeyCode::Right);
        assert_eq!(SelectState::EventAreas(1), app.select_state);
        // EventArea(left-bottom) -> EventArea(right-bottom)
        app.select_state = SelectState::EventAreas(2);
        app.rotate_select_state(KeyCode::Right);
        assert_eq!(SelectState::EventAreas(3), app.select_state);
        // EventArea(right-bottom) -> no change
        app.select_state = SelectState::EventAreas(3);
        app.rotate_select_state(KeyCode::Right);
        assert_eq!(SelectState::EventAreas(3), app.select_state);
        // KeyCode::Down
        // EventArea(left-top) -> EventArea(left-bottom)
        app.select_state = SelectState::EventAreas(0);
        app.rotate_select_state(KeyCode::Down);
        assert_eq!(SelectState::EventAreas(2), app.select_state);
        // EventArea(right-top) -> EventArea(right-bottom)
        app.select_state = SelectState::EventAreas(1);
        app.rotate_select_state(KeyCode::Down);
        assert_eq!(SelectState::EventAreas(3), app.select_state);
        // KeyCode::Up
        // EventArea(left-bottom) -> EventArea(left-top)
        app.select_state = SelectState::EventAreas(2);
        app.rotate_select_state(KeyCode::Up);
        assert_eq!(SelectState::EventAreas(0), app.select_state);
        // EventArea(right-bottom) -> EventArea(right-top)
        app.select_state = SelectState::EventAreas(3);
        app.rotate_select_state(KeyCode::Up);
        assert_eq!(SelectState::EventAreas(1), app.select_state);
    }

    #[test]
    fn test_get_next_idx() {
        let mut app: App<TestBackend> = App::default();
        app.free_idx = [false, true, false, true];
        let result = app.get_next_idx().unwrap();
        assert_eq!(1, result);
        app.free_idx = [false, false, false, false];
        assert!(app.get_next_idx().is_err());
    }
}