chloe_todo_tui 0.1.0

A terminal-based todo application with TUI
Documentation
use std::time::Instant;

use crate::database::{
    models::todos::Todo,
    repository::{NewTodoDraft, Priority},
};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputMode {
    Normal,
    Adding,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FormField {
    Title,
    Description,
    Priority,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Filter {
    All,
    Active,
    Completed,
}

impl Filter {
    pub const fn label(self) -> &'static str {
        match self {
            Filter::All => "All",
            Filter::Active => "Active",
            Filter::Completed => "Completed",
        }
    }

    pub fn next(self) -> Self {
        match self {
            Filter::All => Filter::Active,
            Filter::Active => Filter::Completed,
            Filter::Completed => Filter::All,
        }
    }

    pub fn matches(self, todo: &Todo) -> bool {
        match self {
            Filter::All => true,
            Filter::Active => !todo.completed,
            Filter::Completed => todo.completed,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StatusKind {
    Info,
    Success,
    Error,
}

#[derive(Clone, Debug)]
pub struct StatusBanner {
    pub kind: StatusKind,
    pub message: String,
    pub created_at: Instant,
}

impl StatusBanner {
    pub fn new(kind: StatusKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
            created_at: Instant::now(),
        }
    }
}

pub struct App {
    pub todos: Vec<Todo>,
    pub selected: usize,
    pub filter: Filter,
    pub mode: InputMode,
    pub active_field: FormField,
    pub form: NewTodoDraft,
    pub status: Option<StatusBanner>,
    pub should_quit: bool,
    pub is_loading: bool,
    pub terminal_size: (u16, u16),
}

impl Default for App {
    fn default() -> Self {
        Self {
            todos: Vec::new(),
            selected: 0,
            filter: Filter::All,
            mode: InputMode::Normal,
            active_field: FormField::Title,
            form: NewTodoDraft::default(),
            status: None,
            should_quit: false,
            is_loading: false,
            terminal_size: (0, 0),
        }
    }
}

impl App {
    pub fn with_todos(todos: Vec<Todo>) -> Self {
        let mut app = Self::default();
        app.set_todos(todos);
        app
    }

    pub fn filtered_todos(&self) -> Vec<&Todo> {
        self.todos
            .iter()
            .filter(|t| self.filter.matches(t))
            .collect()
    }

    pub fn filtered_len(&self) -> usize {
        self.todos.iter().filter(|t| self.filter.matches(t)).count()
    }

    pub fn selected_todo(&self) -> Option<&Todo> {
        self.todos
            .iter()
            .filter(|t| self.filter.matches(t))
            .nth(self.selected)
    }

    pub fn selected_todo_id(&self) -> Option<i32> {
        self.selected_todo().map(|todo| todo.id)
    }

    pub fn next(&mut self) {
        let len = self.filtered_len();
        if len == 0 {
            self.selected = 0;
            return;
        }
        self.selected = (self.selected + 1) % len;
    }

    pub fn previous(&mut self) {
        let len = self.filtered_len();
        if len == 0 {
            self.selected = 0;
            return;
        }
        if self.selected == 0 {
            self.selected = len.saturating_sub(1);
        } else {
            self.selected -= 1;
        }
    }

    pub fn set_todos(&mut self, todos: Vec<Todo>) {
        self.todos = todos;
        let len = self.filtered_len();
        if len == 0 {
            self.selected = 0;
        } else if self.selected >= len {
            self.selected = len - 1;
        }
    }

    pub fn cycle_filter(&mut self) {
        self.filter = self.filter.next();
        self.selected = 0;
    }

    pub fn set_status(&mut self, kind: StatusKind, message: impl Into<String>) {
        self.status = Some(StatusBanner::new(kind, message));
    }

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

    pub fn set_loading(&mut self, value: bool) {
        self.is_loading = value;
    }

    pub fn enter_add_mode(&mut self) {
        if self.mode != InputMode::Adding {
            self.reset_form();
        }
        self.mode = InputMode::Adding;
        self.active_field = FormField::Title;
    }

    pub fn exit_add_mode(&mut self) {
        self.mode = InputMode::Normal;
        self.active_field = FormField::Title;
    }

    pub fn toggle_field(&mut self) {
        self.active_field = match self.active_field {
            FormField::Title => FormField::Description,
            FormField::Description => FormField::Priority,
            FormField::Priority => FormField::Title,
        };
    }

    fn active_buffer(&mut self) -> &mut String {
        match self.active_field {
            FormField::Title => &mut self.form.title,
            FormField::Description => &mut self.form.description,
            FormField::Priority => panic!("Priority field does not have a string buffer"),
        }
    }

    pub fn push_char(&mut self, ch: char) {
        self.active_buffer().push(ch);
    }

    pub fn erase_char(&mut self) {
        self.active_buffer().pop();
    }

    pub fn consume_form(&self) -> Option<NewTodoDraft> {
        if self.form.is_valid() {
            Some(self.form.clone())
        } else {
            None
        }
    }

    pub fn reset_form(&mut self) {
        self.form.clear();
        self.active_field = FormField::Title;
    }

    pub fn set_priority_high(&mut self) {
        self.form.priority = Priority::High;
    }

    pub fn set_priority_medium(&mut self) {
        self.form.priority = Priority::Medium;
    }

    pub fn set_priority_low(&mut self) {
        self.form.priority = Priority::Low;
    }

    pub fn increase_priority(&mut self) {
        self.form.priority = self.form.priority.increase();
    }

    pub fn decrease_priority(&mut self) {
        self.form.priority = self.form.priority.decrease();
    }

    pub fn set_terminal_size(&mut self, size: (u16, u16)) {
        self.terminal_size = size;
    }
    pub fn get_terminal_size(&self) -> (u16, u16) {
        self.terminal_size
    }
}