todo_cli_manikya/
state.rs

1use std::collections::HashMap;
2
3use chrono::{DateTime, Local};
4use tui_widget_list::Listable;
5
6use crate::{get_id, ui::render_list_item, Id};
7
8/// Structure of a single task
9#[derive(Default, serde::Serialize, serde::Deserialize)]
10pub struct Task {
11    pub id: Id,
12    pub desc: String,
13    pub completed: bool,
14    pub last_updated: DateTime<Local>,
15}
16
17impl Task {
18    fn new(task: &str) -> Self {
19        Self {
20            id: get_id(),
21            desc: task.to_owned(),
22            completed: false,
23            last_updated: Local::now(),
24        }
25    }
26    fn mark_complete(&mut self) {
27        self.completed = true;
28    }
29    fn mark_incomplete(&mut self) {
30        self.completed = false;
31    }
32}
33
34/// wrapper for a task as a list item
35pub struct ListItem {
36    pub task: Task,
37    pub selected: bool,
38}
39
40impl Listable for &ListItem {
41    fn height(&self) -> usize {
42        1
43    }
44    fn highlight(self) -> Self
45    where
46        Self: Sized,
47    {
48        self
49    }
50}
51
52impl ratatui::widgets::Widget for &ListItem {
53    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) {
54        render_list_item(self, area, buf);
55    }
56}
57
58impl ListItem {
59    pub fn from(task: &Task) -> Self {
60        Self {
61            selected: false,
62            task: Task {
63                completed: task.completed,
64                desc: task.desc.clone(),
65                id: task.id,
66                last_updated: task.last_updated,
67            },
68        }
69    }
70    fn set_selected(&mut self) {
71        self.selected = true;
72    }
73    fn set_unselected(&mut self) {
74        self.selected = false;
75    }
76}
77
78/// The overall state of application
79pub struct State {
80    pub ids: Vec<Id>,
81    pub tasks: HashMap<Id, ListItem>,
82    /// index of selected task
83    pub selected: Option<usize>,
84}
85
86impl State {
87    pub fn new() -> Self {
88        Self {
89            ids: Vec::new(),
90            tasks: HashMap::new(),
91            selected: None,
92        }
93    }
94
95    /// Move app state selection
96    pub fn move_selection(&mut self, upwards: bool) {
97        if let Some(selected) = &self.selected {
98            let next = {
99                if upwards {
100                    selected.saturating_sub(1)
101                } else {
102                    *selected + 1
103                }
104            }
105            .clamp(0, self.ids.len() - 1);
106            self.tasks
107                .get_mut(&self.ids[*selected])
108                .unwrap()
109                .set_unselected();
110            self.tasks.get_mut(&self.ids[next]).unwrap().set_selected();
111            self.selected = Some(next);
112        } else {
113            self.tasks.get_mut(&self.ids[0]).unwrap().set_selected();
114            self.selected = Some(0);
115        }
116    }
117
118    /// Add a new task to the given state
119    pub fn add_task(&mut self, new_task: &str) {
120        let new_id = get_id();
121        self.ids.insert(0, new_id);
122        self.tasks
123            .insert(new_id, ListItem::from(&Task::new(new_task)));
124    }
125
126    /// remove task with given id
127    pub fn remove_task(&mut self, id: &Id) -> Option<()> {
128        if self.tasks.remove(id).is_some() {
129            self.ids.retain(|old_id| old_id != id);
130            Some(())
131        } else {
132            None
133        }
134    }
135
136    /// delete a particular task at an index from the given state
137    pub fn remove_task_by_seq(&mut self, idx: usize) {
138        if idx >= self.tasks.len() {
139            return;
140        }
141        self.remove_task(&self.ids[idx].clone());
142    }
143
144    /// mark incomplete task complete and vice versa
145    ///
146    /// returns true if task marked as complete else false
147    pub fn toggle_task_status(&mut self, idx: usize) -> Option<bool> {
148        let id = self.ids[idx];
149        self.toggle_task_status_by_id(id)
150    }
151
152    /// mark incomplete task complete and vice versa
153    ///
154    /// returns true if task marked as complete else false
155    pub fn toggle_task_status_by_id(&mut self, id: Id) -> Option<bool> {
156        if let Some(list_item) = self.tasks.get_mut(&id) {
157            if list_item.task.completed {
158                list_item.task.mark_incomplete();
159                Some(false)
160            } else {
161                list_item.task.mark_complete();
162                Some(true)
163            }
164        } else {
165            None
166        }
167    }
168
169    /// Return all the tasks as a vector of tasks
170    pub fn get_tasks(&self) -> Vec<&Task> {
171        let mut ans = Vec::new();
172        for id in &self.ids {
173            ans.push(&self.tasks.get(id).unwrap().task);
174        }
175        ans
176    }
177}
178
179impl Default for State {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn check_selection_change() {
191        let mut state = State::new();
192        state.add_task("abc");
193        state.add_task("123");
194        state.add_task("xyz");
195        assert!(state.selected.is_none());
196        state.move_selection(true);
197        assert!(state.selected.is_some());
198        assert_eq!(state.selected.unwrap(), 0);
199        state.move_selection(false);
200        assert_eq!(state.selected.unwrap(), 1);
201        assert!(state.tasks.get(&state.ids[1]).unwrap().selected);
202    }
203}