kanban-tui 0.7.0

Terminal user interface for the kanban project management tool
Documentation
use crate::card_list::{CardList, CardListId};
use crossterm::event::KeyCode;
use uuid::Uuid;

#[derive(Debug, Clone, PartialEq)]
pub enum CardListAction {
    Select(Uuid),
    Edit(Uuid),
    Complete(Uuid),
    TogglePriority(Uuid),
    AssignSprint(Uuid),
    ReassignSprint(Uuid),
    Sort,
    OrderCards,
    MoveColumn(Uuid, bool),
    Create,
    ToggleMultiSelect(Uuid),
    ClearMultiSelect,
    SelectAll,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CardListActionType {
    Navigation,
    Selection,
    Editing,
    Completion,
    Priority,
    Sprint,
    Sorting,
    Movement,
    Creation,
    MultiSelect,
}

pub struct CardListComponentConfig {
    pub enabled_actions: Vec<CardListActionType>,
    pub allow_multi_select: bool,
    pub allow_reordering: bool,
    pub allow_movement: bool,
    pub show_sprint_names: bool,
}

impl Default for CardListComponentConfig {
    fn default() -> Self {
        Self {
            enabled_actions: vec![
                CardListActionType::Navigation,
                CardListActionType::Selection,
                CardListActionType::Editing,
                CardListActionType::Completion,
                CardListActionType::Priority,
                CardListActionType::Sprint,
                CardListActionType::Sorting,
                CardListActionType::Movement,
                CardListActionType::Creation,
                CardListActionType::MultiSelect,
            ],
            allow_multi_select: true,
            allow_reordering: true,
            allow_movement: true,
            show_sprint_names: true,
        }
    }
}

impl CardListComponentConfig {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_actions(mut self, actions: Vec<CardListActionType>) -> Self {
        self.enabled_actions = actions;
        self
    }

    pub fn with_multi_select(mut self, allow: bool) -> Self {
        self.allow_multi_select = allow;
        self
    }

    pub fn with_reordering(mut self, allow: bool) -> Self {
        self.allow_reordering = allow;
        self
    }

    pub fn with_movement(mut self, allow: bool) -> Self {
        self.allow_movement = allow;
        self
    }

    pub fn with_sprint_names(mut self, show: bool) -> Self {
        self.show_sprint_names = show;
        self
    }

    pub fn is_action_enabled(&self, action_type: &CardListActionType) -> bool {
        self.enabled_actions.contains(action_type)
    }

    pub fn help_text(&self) -> String {
        let mut parts = vec!["ESC: cancel"];

        if self.is_action_enabled(&CardListActionType::Navigation) {
            parts.push("j/k: navigate");
        }

        if self.is_action_enabled(&CardListActionType::Selection) {
            parts.push("Enter/Space: select");
        }

        if self.is_action_enabled(&CardListActionType::Editing) {
            parts.push("e: edit");
        }

        if self.is_action_enabled(&CardListActionType::Completion) {
            parts.push("c: complete");
        }

        if self.is_action_enabled(&CardListActionType::Priority) {
            parts.push("p: priority");
        }

        if self.is_action_enabled(&CardListActionType::Sprint) {
            parts.push("a: assign sprint");
        }

        if self.is_action_enabled(&CardListActionType::Sorting) {
            parts.push("o: sort");
        }

        if self.is_action_enabled(&CardListActionType::Movement) {
            parts.push("H/L: move");
        }

        if self.is_action_enabled(&CardListActionType::Creation) {
            parts.push("n: new");
        }

        if self.allow_multi_select {
            parts.push("v: select card | V: multi-select");
        }

        parts.join(" | ")
    }
}

pub struct CardListComponent {
    pub card_list: CardList,
    pub config: CardListComponentConfig,
    pub multi_selected: std::collections::HashSet<Uuid>,
    pub viewport_height: usize,
}

impl CardListComponent {
    pub fn new(list_id: CardListId, config: CardListComponentConfig) -> Self {
        Self {
            card_list: CardList::new(list_id),
            config,
            multi_selected: std::collections::HashSet::new(),
            viewport_height: 20,
        }
    }

    pub fn with_config(list_id: CardListId, config: CardListComponentConfig) -> Self {
        Self::new(list_id, config)
    }

    pub fn update_cards(&mut self, cards: Vec<Uuid>) {
        self.card_list.update_cards(cards);
    }

    pub fn get_selected_card_id(&self) -> Option<Uuid> {
        self.card_list.get_selected_card_id()
    }

    pub fn get_multi_selected(&self) -> Vec<Uuid> {
        self.multi_selected.iter().copied().collect()
    }

    pub fn toggle_multi_select(&mut self, card_id: Uuid) {
        if self.config.allow_multi_select {
            if self.multi_selected.contains(&card_id) {
                self.multi_selected.remove(&card_id);
            } else {
                self.multi_selected.insert(card_id);
            }
        }
    }

    pub fn clear_multi_select(&mut self) {
        self.multi_selected.clear();
    }

    pub fn select_all(&mut self) {
        if self.config.allow_multi_select {
            for card_id in &self.card_list.cards {
                self.multi_selected.insert(*card_id);
            }
        }
    }

    pub fn navigate_up(&mut self) -> bool {
        self.card_list.navigate_up()
    }

    pub fn navigate_down(&mut self) -> bool {
        self.card_list.navigate_down()
    }

    pub fn is_empty(&self) -> bool {
        self.card_list.is_empty()
    }

    pub fn len(&self) -> usize {
        self.card_list.len()
    }

    pub fn get_selected_index(&self) -> Option<usize> {
        self.card_list.get_selected_index()
    }

    pub fn set_selected_index(&mut self, index: Option<usize>) {
        self.card_list.set_selected_index(index);
    }

    pub fn get_scroll_offset(&self) -> usize {
        self.card_list.get_scroll_offset()
    }

    pub fn set_scroll_offset(&mut self, offset: usize) {
        self.card_list.set_scroll_offset(offset);
    }

    pub fn ensure_selected_visible(&mut self, viewport_height: usize) {
        self.card_list.ensure_selected_visible(viewport_height);
    }

    pub fn help_text(&self) -> String {
        self.config.help_text()
    }

    pub fn handle_key(&mut self, key: KeyCode) -> Option<CardListAction> {
        match key {
            KeyCode::Char('j') | KeyCode::Down => {
                if self
                    .config
                    .is_action_enabled(&CardListActionType::Navigation)
                {
                    self.navigate_down();
                }
                None
            }
            KeyCode::Char('k') | KeyCode::Up => {
                if self
                    .config
                    .is_action_enabled(&CardListActionType::Navigation)
                {
                    self.navigate_up();
                }
                None
            }
            KeyCode::Enter | KeyCode::Char(' ') => {
                if self
                    .config
                    .is_action_enabled(&CardListActionType::Selection)
                {
                    self.get_selected_card_id().map(CardListAction::Select)
                } else {
                    None
                }
            }
            KeyCode::Char('e') => {
                if self.config.is_action_enabled(&CardListActionType::Editing) {
                    self.get_selected_card_id().map(CardListAction::Edit)
                } else {
                    None
                }
            }
            KeyCode::Char('c') => {
                if self
                    .config
                    .is_action_enabled(&CardListActionType::Completion)
                {
                    self.get_selected_card_id().map(CardListAction::Complete)
                } else {
                    None
                }
            }
            KeyCode::Char('p') => {
                if self.config.is_action_enabled(&CardListActionType::Priority) {
                    self.get_selected_card_id()
                        .map(CardListAction::TogglePriority)
                } else {
                    None
                }
            }
            KeyCode::Char('a') => {
                if self.config.is_action_enabled(&CardListActionType::Sprint) {
                    self.get_selected_card_id()
                        .map(CardListAction::AssignSprint)
                } else {
                    None
                }
            }
            KeyCode::Char('S') => {
                if self.config.is_action_enabled(&CardListActionType::Sprint) {
                    self.get_selected_card_id()
                        .map(CardListAction::ReassignSprint)
                } else {
                    None
                }
            }
            KeyCode::Char('o') => {
                if self.config.is_action_enabled(&CardListActionType::Sorting) {
                    Some(CardListAction::Sort)
                } else {
                    None
                }
            }
            KeyCode::Char('O') => {
                if self.config.is_action_enabled(&CardListActionType::Sorting) {
                    Some(CardListAction::OrderCards)
                } else {
                    None
                }
            }
            KeyCode::Char('H') => {
                if self.config.is_action_enabled(&CardListActionType::Movement)
                    && self.config.allow_movement
                {
                    self.get_selected_card_id()
                        .map(|id| CardListAction::MoveColumn(id, false))
                } else {
                    None
                }
            }
            KeyCode::Char('L') => {
                if self.config.is_action_enabled(&CardListActionType::Movement)
                    && self.config.allow_movement
                {
                    self.get_selected_card_id()
                        .map(|id| CardListAction::MoveColumn(id, true))
                } else {
                    None
                }
            }
            KeyCode::Char('n') => {
                if self.config.is_action_enabled(&CardListActionType::Creation) {
                    Some(CardListAction::Create)
                } else {
                    None
                }
            }
            KeyCode::Char('v') => {
                if self
                    .config
                    .is_action_enabled(&CardListActionType::MultiSelect)
                {
                    self.get_selected_card_id()
                        .map(CardListAction::ToggleMultiSelect)
                } else {
                    None
                }
            }
            KeyCode::Char('V') => {
                if self
                    .config
                    .is_action_enabled(&CardListActionType::MultiSelect)
                    && self.config.allow_multi_select
                {
                    Some(CardListAction::SelectAll)
                } else {
                    None
                }
            }
            _ => None,
        }
    }
}