From ae99fa193ccc457ab785c7f9e741d691b6035560 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 5 Dec 2022 02:37:18 +0400 Subject: [PATCH] Added task shifting up and down and left and right. Francesco added some stuff as well. --- Cargo.lock | 8 ++ Cargo.toml | 6 +- src/app.rs | 198 +++++++++++++++++++++++++++++++++++++++++++++++ src/app/tests.rs | 1 + src/input.rs | 10 ++- src/lib.rs | 7 ++ src/main.rs | 18 ++--- src/types.rs | 123 ----------------------------- src/ui.rs | 8 +- 9 files changed, 236 insertions(+), 143 deletions(-) create mode 100644 src/app.rs create mode 100644 src/app/tests.rs create mode 100644 src/lib.rs delete mode 100644 src/types.rs diff --git a/Cargo.lock b/Cargo.lock index b076b36..cc2c23f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + [[package]] name = "autocfg" version = "1.1.0" @@ -99,11 +105,13 @@ checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" name = "kanban-tui" version = "0.1.0" dependencies = [ + "anyhow", "crossterm", "indexmap", "int-enum", "serde", "serde_json", + "thiserror", "tui", ] diff --git a/Cargo.toml b/Cargo.toml index c3f4384..b2248af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,8 @@ crossterm = "0.25" serde = { version = "1.0.148" , features = [ "derive" ] } serde_json = "1.0.89" indexmap = { version = "1.9.2" , features = [ "serde" ] } -int-enum = "0.5.0" \ No newline at end of file +int-enum = "0.5.0" + +#error handling +thiserror = "1" +anyhow = "1" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..232e8e9 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,198 @@ +use std::cmp::min; +use indexmap::IndexMap; +use int_enum::IntEnum; +use serde::{Deserialize, Serialize}; + +#[cfg(test)] +mod tests; + +#[repr(usize)] +#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, IntEnum)] +pub enum TaskStatus { + Todo = 0, + InProgress = 1, + Done = 2, + Ideas = 3, +} + +// #[derive(Deserialize, Serialize, Debug, Clone, Copy)] +#[derive(Deserialize, Serialize, Debug)] +pub struct Task { + pub title: String, + pub description: String, +} + +impl Default for Task { + fn default() -> Self { + Task { + title: String::new(), + description: String::new(), + } + } +} + +/// Type used mainly for serialization at this time +#[derive(Deserialize, Serialize, Debug)] +pub struct Project { + pub name: String, + pub tasks_per_column: IndexMap>, +} + +#[derive(Debug, thiserror::Error)] +pub enum KanbanError { + #[error("There is something wrong with the json schema, it doesn't match Project struct")] + BadJson, + #[error("Some form of IO error occured: {0}")] + Io(#[from] std::io::Error), +} + +impl Project { + pub fn new(name: &str) -> Self { + Project { + name: name.to_owned(), + tasks_per_column: IndexMap::from( + [(TaskStatus::Done, vec![]), + (TaskStatus::Todo, vec![]), + (TaskStatus::InProgress, vec![]), + (TaskStatus::Ideas, vec![])], + ), + } + } + + fn load_from_json(json: &str) -> Result { + serde_json::from_str(json).map_err(|_| KanbanError::BadJson) + } + + pub fn load() -> Result { + let json = std::fs::read_to_string("kanban-tui.json")?; + Self::load_from_json(&json) + } + + pub fn add_task(&mut self, status: TaskStatus, task: Task) { + self.tasks_per_column.entry(status).or_default().push(task); + } + + /// Comment out cause this is dangerous + pub fn save() { + // let mut project = Project::new("Kanban Tui"); + // project.add_task(Task::default()); + // project.add_task(Task::default()); + // let json = serde_json::to_string_pretty(&project).unwrap(); + // std::fs::write("./project.json", json).unwrap(); + } +} + +impl Default for Project { + fn default() -> Self { + Project { + name: String::new(), + tasks_per_column: IndexMap::new(), + } + } +} + +pub struct AppState { + pub selected_column: usize, + pub selected_task: [usize; 4], + pub project: Project, + pub quit: bool, +} + +impl AppState { + pub fn new(project: Project) -> Self { + AppState { + selected_column: 0, + selected_task: [0, 0, 0, 0], + quit: false, + project, + } + } + + fn selected_task_idx(&self) -> usize { + self.selected_task[self.selected_column] + } + + fn selected_task_idx_mut(&mut self) -> &mut usize { + &mut self.selected_task[self.selected_column] + } + + pub fn get_tasks_in_active_column(&self) -> &[Task] { + let column: TaskStatus = TaskStatus::from_int(self.selected_column).unwrap().clone(); + self.project.tasks_per_column.get(&column).unwrap() + } + + pub fn get_tasks_in_active_column_mut(&mut self) -> &mut Vec { + let column: TaskStatus = TaskStatus::from_int(self.selected_column).unwrap().clone(); + self.project.tasks_per_column.get_mut(&column).unwrap() + } + + pub fn get_selected_task(&self) -> Option<&Task> { + let tasks = self.get_tasks_in_active_column(); + tasks.get(self.selected_task_idx()) + } + + pub fn select_previous_task(&mut self) { + *self.selected_task_idx_mut() = self.selected_task_idx().saturating_sub(1) + } + + pub fn select_next_task(&mut self) { + let tasks = self.get_tasks_in_active_column(); + if tasks.len() > 0 { + let mins = min(self.selected_task_idx() + 1, tasks.len() - 1); + *self.selected_task_idx_mut() = mins; + } + } + + pub fn select_previous_column(&mut self) { + self.selected_column = self.selected_column.saturating_sub(1); + } + + pub fn select_next_column(&mut self) { + self.selected_column = min(self.selected_column + 1, self.project.tasks_per_column.len() - 1) + } + + pub fn move_task_previous_column(&mut self) { + let tasks = self.get_tasks_in_active_column(); + let task_idx = self.selected_task_idx(); + if self.selected_column > 0 && tasks.len() > 0 && task_idx.clone() < tasks.len() { + let task = self.get_tasks_in_active_column_mut().remove(task_idx); + *self.selected_task_idx_mut() = self.selected_task_idx().saturating_sub(1); + self.select_previous_column(); + let target_tasks = self.get_tasks_in_active_column_mut(); + target_tasks.push(task); + *self.selected_task_idx_mut() = target_tasks.len() - 1; + } + } + + pub fn move_task_next_column(&mut self) { + let tasks = self.get_tasks_in_active_column(); + let task_idx = self.selected_task_idx(); + if self.selected_column < self.project.tasks_per_column.len() && tasks.len() > 0 && task_idx < tasks.len() { + let task = self.get_tasks_in_active_column_mut().remove(task_idx); + *self.selected_task_idx_mut() = self.selected_task_idx().saturating_sub(1); + self.select_next_column(); + let target_tasks = self.get_tasks_in_active_column_mut(); + target_tasks.push(task); + *self.selected_task_idx_mut() = target_tasks.len() - 1; + } + } + + pub fn move_task_up(&mut self) { + let task_idx = self.selected_task_idx(); + if task_idx > 0 { + let tasks = self.get_tasks_in_active_column_mut(); + tasks.swap(task_idx, task_idx - 1); + *self.selected_task_idx_mut() = task_idx - 1; + } + } + + pub fn move_task_down(&mut self) { + let task_idx = self.selected_task_idx(); + let tasks = self.get_tasks_in_active_column_mut(); + if task_idx < tasks.len() - 1 { + tasks.swap(task_idx, task_idx + 1); + *self.selected_task_idx_mut() = task_idx + 1; + } + } +} + diff --git a/src/app/tests.rs b/src/app/tests.rs new file mode 100644 index 0000000..9148c04 --- /dev/null +++ b/src/app/tests.rs @@ -0,0 +1 @@ +use super::*; \ No newline at end of file diff --git a/src/input.rs b/src/input.rs index 953d720..65b2b6b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,6 +1,6 @@ use crossterm::event; use crossterm::event::{Event, KeyCode}; -use crate::types::{AppState}; +use crate::app::{AppState}; pub fn handle_input(state: &mut AppState) -> Result<(), std::io::Error> { if let Event::Key(key) = event::read()? { @@ -14,6 +14,14 @@ pub fn handle_input(state: &mut AppState) -> Result<(), std::io::Error> { KeyCode::Up => state.select_previous_task(), KeyCode::Char('l') | KeyCode::Right => state.select_next_column(), + KeyCode::Char('<') | + KeyCode::Char('H') => state.move_task_previous_column(), + KeyCode::Char('>') | + KeyCode::Char('L') => state.move_task_next_column(), + KeyCode::Char('=') | + KeyCode::Char('J') => state.move_task_down(), + KeyCode::Char('-') | + KeyCode::Char('K') => state.move_task_up(), _ => {} } } diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..36766b7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +mod app; +mod ui; +mod input; + +pub use app::*; +pub use ui::draw; +pub use input::handle_input; diff --git a/src/main.rs b/src/main.rs index a0e2aa6..6b3d25d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,29 +1,23 @@ #![allow(dead_code)] -mod ui; -mod types; -mod input; +use kanban_tui::{AppState, Project}; use std::{io}; use crossterm::{event::*, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}}; use tui::backend::CrosstermBackend; use tui::Terminal; -use crate::input::handle_input; -use crate::types::*; -fn main() -> Result<(), io::Error> { - // setup terminal +fn main() -> anyhow::Result<()> { enable_raw_mode()?; let mut stdout = io::stdout(); crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let mut state = AppState::new(Project::load()); + let mut state = AppState::new(Project::load()?); - loop { - terminal.draw(|f| ui::draw(f, &mut state))?; - handle_input(&mut state)?; - if state.quit { break } + while !state.quit { + terminal.draw(|f| kanban_tui::draw(f, &mut state))?; + kanban_tui::handle_input(&mut state)?; } // restore terminal diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 53df369..0000000 --- a/src/types.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::cmp::min; -use indexmap::IndexMap; -use int_enum::IntEnum; -use serde::{Deserialize, Serialize}; - -#[repr(usize)] -#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, IntEnum)] -pub enum TaskStatus { - Todo = 0, - InProgress = 1, - Done = 2, - Ideas = 3, -} - -// #[derive(Deserialize, Serialize, Debug, Clone, Copy)] -#[derive(Deserialize, Serialize, Debug)] -pub struct Task { - pub title: String, - pub description: String, -} - -impl Default for Task { - fn default() -> Self { - Task { - title: String::new(), - description: String::new(), - } - } -} - -/// Type used mainly for serialization at this time -#[derive(Deserialize, Serialize, Debug)] -pub struct Project { - pub name: String, - pub tasks_per_column: IndexMap>, -} - -impl Project { - fn new(name: &str) -> Self { - Project { - name: name.to_owned(), - tasks_per_column: IndexMap::from( - [(TaskStatus::Done, vec![]), - (TaskStatus::Todo, vec![]), - (TaskStatus::InProgress, vec![]), - (TaskStatus::Ideas, vec![])], - ), - } - } - - pub fn load() -> Self { - let json = std::fs::read_to_string("kanban-tui.json") - .expect("Could not read json file"); - serde_json::from_str(&json) - .expect("There is something wrong with the json schema, it doesn't match Project struct") - } - - pub fn add_task(&mut self, status: TaskStatus, task: Task) { - self.tasks_per_column.entry(status).or_default().push(task); - } - - /// Comment out cause this is dangerous - pub fn save() { - // let mut project = Project::new("Kanban Tui"); - // project.add_task(Task::default()); - // project.add_task(Task::default()); - // let json = serde_json::to_string_pretty(&project).unwrap(); - // std::fs::write("./project.json", json).unwrap(); - } -} - -impl Default for Project { - fn default() -> Self { - Project { - name: String::new(), - tasks_per_column: IndexMap::new(), - } - } -} - -pub struct AppState { - pub selected_column: usize, - pub selected_task: [usize; 4], - pub project: Project, - pub quit: bool, -} - -impl AppState { - pub fn new(project: Project) -> Self { - AppState { - selected_column: 0, - selected_task: [0, 0, 0, 0], - quit: false, - project: project, - } - } - - pub fn get_tasks_in_active_column(&self) -> &Vec { - let column: TaskStatus = TaskStatus::from_int(self.selected_column).unwrap().clone(); - self.project.tasks_per_column.get(&column).unwrap() - } - - pub fn select_previous_task(&mut self) { - self.selected_task[self.selected_column] = self.selected_task[self.selected_column].saturating_sub(1) - } - - pub fn select_next_task(&mut self) { - let tasks = self.get_tasks_in_active_column(); - if tasks.len() > 0 { - let mins = min(self.selected_task[self.selected_column] + 1, tasks.len() - 1); - self.selected_task[self.selected_column] = mins; - } - } - - pub fn select_previous_column(&mut self) { - self.selected_column = self.selected_column.saturating_sub(1); - } - - pub fn select_next_column(&mut self) { - self.selected_column = min(self.selected_column + 1, self.project.tasks_per_column.len() - 1) - } -} - diff --git a/src/ui.rs b/src/ui.rs index 51b0a0d..2ad0e45 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,8 +4,7 @@ use tui::{Frame}; use tui::style::{Color, Modifier, Style}; use tui::text::{Span, Spans}; use tui::widgets::*; -use crate::types::*; -use int_enum::IntEnum; +use crate::app::*; fn draw_tasks(f: &mut Frame, area: &Rect, state: &AppState) { let columns = Layout::default() @@ -47,10 +46,7 @@ fn draw_task_info(f: &mut Frame, area: &Rect, state: &AppState) { let block = Block::default() .title("TASK INFO") .borders(Borders::ALL); - let column: TaskStatus = TaskStatus::from_int(state.selected_column).unwrap(); - let tasks = state.project.tasks_per_column.get(&column).unwrap(); - if tasks.len() > 0 { - let task: &Task = &tasks[state.selected_task[state.selected_column]]; + if let Some(task) = state.get_selected_task() { let p = Paragraph::new(task.description.as_str()).block(block).wrap(Wrap { trim: true }); f.render_widget(p, *area); } else {