aven 0.1.6

Local-first task manager CLI and sync server
Documentation
use anyhow::Result;
use tokio::task::JoinHandle;

use crate::query::{self, SortDirection, TaskSearchQuery};

use super::{
    SidebarEntryTarget, TaskFilterModifiers, TaskOrder, TaskScope, TaskScopeTarget, TaskView,
    TuiStore,
};

async fn search_preview_with_pool(
    pool: sqlx::SqlitePool,
    workspace_id: String,
    input: String,
    limit: usize,
) -> Result<query::TaskSearchPreviewResultSet> {
    let text = input.trim().to_string();
    if text.is_empty() {
        return Ok(query::TaskSearchPreviewResultSet {
            items: Vec::new(),
            total_matches: 0,
        });
    }
    let mut conn = pool.acquire().await?;
    query::search_task_preview_set_in_workspace(
        &mut conn,
        workspace_id.as_str(),
        TaskSearchQuery {
            text,
            include_deleted: false,
            limit,
        },
    )
    .await
}

impl TuiStore {
    pub(crate) fn sidebar_selection(&self) -> Option<usize> {
        if let Some(index) = self.sidebar_entries.iter().position(|entry| {
            matches!(
                (&entry.target, &self.view_state.scope),
                (
                    Some(SidebarEntryTarget::Scope(TaskScopeTarget::Project(project))),
                    TaskScope::Project(scope),
                ) if project == scope
            )
        }) {
            return Some(index);
        }
        self.sidebar_entries
            .iter()
            .position(|entry| match &entry.target {
                Some(SidebarEntryTarget::View(view)) => *view == self.view_state.view,
                _ => false,
            })
            .or(Some(1))
    }

    pub(crate) async fn show_view(&mut self, view: TaskView) -> Result<Option<usize>> {
        self.view_state.view = view;
        if view != TaskView::Search {
            self.view_state.filter_modifiers.task_ids.clear();
        }
        self.refresh(None).await
    }

    pub(crate) async fn restore_view_state(
        &mut self,
        view_state: super::TaskViewState,
    ) -> Result<super::ScopeRefreshResult> {
        self.view_state = view_state;
        self.refresh_with_scope_fallback(None).await
    }

    pub(crate) async fn show_scope(&mut self, target: TaskScopeTarget) -> Result<Option<usize>> {
        self.view_state.filter_modifiers.task_ids.clear();
        self.view_state.scope = match target {
            TaskScopeTarget::Workspace => TaskScope::Workspace,
            TaskScopeTarget::Project(project) => TaskScope::Project(project),
        };
        self.refresh(None).await
    }

    pub(crate) async fn clear_filters(&mut self) -> Result<Option<usize>> {
        self.view_state.filter_modifiers = TaskFilterModifiers::default();
        self.refresh(None).await
    }

    pub(crate) async fn filter_label(&mut self, label: String) -> Result<Option<usize>> {
        self.view_state.filter_modifiers.label = Some(label);
        self.refresh(None).await
    }

    pub(crate) async fn filter_priority(&mut self, priority: String) -> Result<Option<usize>> {
        self.view_state.filter_modifiers.priority = Some(priority);
        self.refresh(None).await
    }

    pub(crate) async fn toggle_deleted_filter(&mut self) -> Result<Option<usize>> {
        let modifiers = &mut self.view_state.filter_modifiers;
        if modifiers.deleted_only {
            modifiers.deleted_only = false;
            modifiers.include_deleted = false;
        } else if modifiers.include_deleted {
            modifiers.deleted_only = true;
        } else {
            modifiers.include_deleted = true;
        }
        self.refresh(None).await
    }

    #[cfg(test)]
    pub(crate) async fn search_preview(
        &self,
        input: &str,
        limit: usize,
    ) -> Result<query::TaskSearchPreviewResultSet> {
        search_preview_with_pool(
            self.pool.clone(),
            self.active_workspace.id.clone(),
            input.to_string(),
            limit,
        )
        .await
    }

    pub(crate) fn spawn_search_preview(
        &self,
        input: String,
        limit: usize,
    ) -> JoinHandle<Result<query::TaskSearchPreviewResultSet>> {
        tokio::spawn(search_preview_with_pool(
            self.pool.clone(),
            self.active_workspace.id.clone(),
            input,
            limit,
        ))
    }

    pub(crate) async fn accept_search(&mut self, input: &str) -> Result<Option<usize>> {
        let text = input.trim();
        if text.is_empty() {
            self.view_state.filter_modifiers.task_ids.clear();
            self.view_state.view = TaskView::Queue;
            return self.refresh(None).await;
        }
        let mut conn = self.pool.acquire().await?;
        let results = query::search_task_items_in_workspace(
            &mut conn,
            self.active_workspace.id.as_str(),
            TaskSearchQuery {
                text: text.to_string(),
                include_deleted: false,
                limit: 100,
            },
        )
        .await?;
        drop(conn);
        self.view_state.scope = TaskScope::Workspace;
        self.view_state.view = TaskView::Search;
        self.view_state.filter_modifiers = TaskFilterModifiers {
            task_ids: results
                .iter()
                .map(|result| result.item.task.id.clone())
                .collect(),
            ..TaskFilterModifiers::default()
        };
        self.refresh(None).await
    }

    pub(crate) fn set_view_order(&mut self, order: TaskOrder) {
        if self.view_state.view == TaskView::Queue {
            self.view_state.view = TaskView::Open;
        }
        self.view_state.order = order;
        if order == TaskOrder::Created {
            self.view_state.direction = SortDirection::Desc;
        }
    }

    pub(crate) fn reverse_view_order(&mut self) {
        if self.view_state.view == TaskView::Queue {
            self.view_state.view = TaskView::Open;
        }
        self.view_state.direction = self.view_state.direction.toggled();
    }

    pub(super) fn restored_task_selection(&self, selected_id: Option<&str>) -> Option<usize> {
        if self.tasks.is_empty() {
            return None;
        }
        selected_id
            .and_then(|id| self.tasks.iter().position(|item| item.task.id == id))
            .or(Some(0))
    }

    pub(super) fn restored_task_selection_at_index(
        &self,
        selected: Option<usize>,
    ) -> Option<usize> {
        if self.tasks.is_empty() {
            None
        } else {
            selected.map(|index| index.min(self.tasks.len() - 1))
        }
    }
}