kanban-tui 0.7.0

Terminal user interface for the kanban project management tool
Documentation
use crate::card_list::{CardList, CardListId};
use crate::view_strategy::ViewRefreshContext;
use kanban_domain::CardQueryBuilder;
use uuid::Uuid;

fn build_query<'a>(ctx: &'a ViewRefreshContext<'a>) -> CardQueryBuilder<'a> {
    let mut builder =
        CardQueryBuilder::new(ctx.all_cards, ctx.all_columns, ctx.all_sprints, ctx.board);
    if !ctx.active_sprint_filters.is_empty() {
        builder = builder.in_sprints(ctx.active_sprint_filters.iter().copied());
    }
    if ctx.hide_assigned_cards {
        builder = builder.hide_assigned();
    }
    if let Some(query) = ctx.search_query {
        builder = builder.search(query);
    }
    builder
}

#[derive(Clone)]
pub struct ColumnBoundary {
    pub column_id: Uuid,
    pub column_name: String,
    pub start_index: usize,
    pub card_count: usize,
}

pub trait LayoutStrategy {
    fn get_active_task_list(&self) -> Option<&CardList>;
    fn get_active_task_list_mut(&mut self) -> Option<&mut CardList>;
    fn get_all_task_lists(&self) -> Vec<&CardList>;
    fn navigate_left(&mut self, select_last: bool) -> bool;
    fn navigate_right(&mut self, select_last: bool) -> bool;
    fn refresh_lists(&mut self, ctx: &ViewRefreshContext);
    fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
    fn as_any(&self) -> &dyn std::any::Any;
}

pub struct SingleListLayout {
    task_list: CardList,
}

impl SingleListLayout {
    pub fn new() -> Self {
        Self {
            task_list: CardList::new(CardListId::All),
        }
    }
}

impl Default for SingleListLayout {
    fn default() -> Self {
        Self::new()
    }
}

impl LayoutStrategy for SingleListLayout {
    fn get_active_task_list(&self) -> Option<&CardList> {
        Some(&self.task_list)
    }

    fn get_active_task_list_mut(&mut self) -> Option<&mut CardList> {
        Some(&mut self.task_list)
    }

    fn get_all_task_lists(&self) -> Vec<&CardList> {
        vec![&self.task_list]
    }

    fn navigate_left(&mut self, _select_last: bool) -> bool {
        false
    }

    fn navigate_right(&mut self, _select_last: bool) -> bool {
        false
    }

    fn refresh_lists(&mut self, ctx: &ViewRefreshContext) {
        let card_ids = build_query(ctx).execute();
        self.task_list.update_cards(card_ids);
    }

    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
        self
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

pub struct ColumnListsLayout {
    column_lists: Vec<CardList>,
    active_column_index: usize,
}

impl ColumnListsLayout {
    pub fn new() -> Self {
        Self {
            column_lists: Vec::new(),
            active_column_index: 0,
        }
    }

    pub fn get_active_column_index(&self) -> usize {
        self.active_column_index
    }

    pub fn set_active_column_index(&mut self, index: usize) {
        if index < self.column_lists.len() {
            self.active_column_index = index;
        }
    }
}

impl Default for ColumnListsLayout {
    fn default() -> Self {
        Self::new()
    }
}

impl LayoutStrategy for ColumnListsLayout {
    fn get_active_task_list(&self) -> Option<&CardList> {
        self.column_lists.get(self.active_column_index)
    }

    fn get_active_task_list_mut(&mut self) -> Option<&mut CardList> {
        self.column_lists.get_mut(self.active_column_index)
    }

    fn get_all_task_lists(&self) -> Vec<&CardList> {
        self.column_lists.iter().collect()
    }

    fn navigate_left(&mut self, select_last: bool) -> bool {
        if self.active_column_index > 0 {
            self.active_column_index -= 1;
            if let Some(list) = self.get_active_task_list_mut() {
                if list.is_empty() {
                    list.clear();
                } else if select_last {
                    list.set_selected_index(Some(list.len() - 1));
                } else if list.get_selected_index().is_none() {
                    list.set_selected_index(Some(0));
                }
            }
            true
        } else {
            false
        }
    }

    fn navigate_right(&mut self, select_last: bool) -> bool {
        if self.active_column_index < self.column_lists.len().saturating_sub(1) {
            self.active_column_index += 1;
            if let Some(list) = self.get_active_task_list_mut() {
                if list.is_empty() {
                    list.clear();
                } else if select_last {
                    list.set_selected_index(Some(list.len() - 1));
                } else if list.get_selected_index().is_none() {
                    list.set_selected_index(Some(0));
                }
            }
            true
        } else {
            false
        }
    }

    fn refresh_lists(&mut self, ctx: &ViewRefreshContext) {
        let mut board_columns: Vec<_> = ctx
            .all_columns
            .iter()
            .filter(|col| col.board_id == ctx.board.id)
            .collect();
        board_columns.sort_by_key(|col| col.position);

        let mut new_column_lists = Vec::new();

        for column in board_columns.iter() {
            let card_ids = build_query(ctx).in_column(column.id).execute();

            let existing_list = self
                .column_lists
                .iter()
                .find(|list| list.id == CardListId::Column(column.id));

            let (prev_selected_card, prev_scroll_offset) = if let Some(existing) = existing_list {
                (
                    existing.get_selected_card_id(),
                    existing.get_scroll_offset(),
                )
            } else {
                (None, 0)
            };

            let mut task_list = CardList::new(CardListId::Column(column.id));
            task_list.update_cards(card_ids);
            if let Some(card_id) = prev_selected_card {
                task_list.select_card(card_id);
            }
            task_list.set_scroll_offset(prev_scroll_offset);
            new_column_lists.push(task_list);
        }

        self.column_lists = new_column_lists;

        if self.active_column_index >= self.column_lists.len() {
            self.active_column_index = self.column_lists.len().saturating_sub(1);
        }
    }

    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
        self
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

pub struct VirtualUnifiedLayout {
    unified_list: CardList,
    column_boundaries: Vec<ColumnBoundary>,
}

impl VirtualUnifiedLayout {
    pub fn new() -> Self {
        Self {
            unified_list: CardList::new(CardListId::All),
            column_boundaries: Vec::new(),
        }
    }

    pub fn get_column_boundaries(&self) -> &[ColumnBoundary] {
        &self.column_boundaries
    }
}

impl Default for VirtualUnifiedLayout {
    fn default() -> Self {
        Self::new()
    }
}

impl LayoutStrategy for VirtualUnifiedLayout {
    fn get_active_task_list(&self) -> Option<&CardList> {
        Some(&self.unified_list)
    }

    fn get_active_task_list_mut(&mut self) -> Option<&mut CardList> {
        Some(&mut self.unified_list)
    }

    fn get_all_task_lists(&self) -> Vec<&CardList> {
        vec![&self.unified_list]
    }

    fn navigate_left(&mut self, _select_last: bool) -> bool {
        false
    }

    fn navigate_right(&mut self, _select_last: bool) -> bool {
        false
    }

    fn refresh_lists(&mut self, ctx: &ViewRefreshContext) {
        let mut board_columns: Vec<_> = ctx
            .all_columns
            .iter()
            .filter(|col| col.board_id == ctx.board.id)
            .collect();
        board_columns.sort_by_key(|col| col.position);

        let mut unified_cards = Vec::new();
        let mut new_boundaries = Vec::new();

        for column in board_columns.iter() {
            let card_ids = build_query(ctx).in_column(column.id).execute();
            let card_count = card_ids.len();

            if card_count > 0 {
                new_boundaries.push(ColumnBoundary {
                    column_id: column.id,
                    column_name: column.name.clone(),
                    start_index: unified_cards.len(),
                    card_count,
                });

                unified_cards.extend(card_ids);
            }
        }

        self.unified_list.update_cards(unified_cards);
        self.column_boundaries = new_boundaries;
    }

    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
        self
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}