diff --git a/Cargo.lock b/Cargo.lock index f2bc1ce..6549203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", + "serde", +] + [[package]] name = "itoa" version = "1.0.4" @@ -62,6 +79,7 @@ name = "kanban-tui" version = "0.1.0" dependencies = [ "crossterm", + "indexmap", "serde", "serde_json", "tui", diff --git a/Cargo.toml b/Cargo.toml index 3f97f47..5512311 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,4 @@ tui = "0.19.0" crossterm = "0.25" serde = { version = "1.0.148" , features = [ "derive" ] } serde_json = "1.0.89" +indexmap = { version = "1.9.2" , features = [ "serde" ] } \ No newline at end of file diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..7bb55f9 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,22 @@ +use std::cmp::min; +use crossterm::event; +use crossterm::event::{Event, KeyCode}; +use crate::types::AppState; + +pub fn handle_input(state: &mut AppState) -> Result<(), std::io::Error> { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => state.quit = true, + KeyCode::Char('h') | + KeyCode::Left => state.selected_column = state.selected_column.saturating_sub(1), + KeyCode::Char('j') | + KeyCode::Down => state.selected_task[state.selected_column] += 1, + KeyCode::Char('k') | + KeyCode::Up => state.selected_task[state.selected_column] -= 1, + KeyCode::Char('l') | + KeyCode::Right => state.selected_column = min(state.selected_column + 1, 4), + _ => {} + } + } + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 6567702..a0e2aa6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,14 @@ +#![allow(dead_code)] mod ui; mod types; +mod input; use std::{io}; -use crossterm::{ - event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen} -}; +use crossterm::{event::*, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}}; use tui::backend::CrosstermBackend; use tui::Terminal; -use crate::types::{Project, Task}; +use crate::input::handle_input; +use crate::types::*; fn main() -> Result<(), io::Error> { // setup terminal @@ -18,17 +18,12 @@ fn main() -> Result<(), io::Error> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - let mut project = Project::load(); + let mut state = AppState::new(Project::load()); loop { - terminal.draw(|f| ui::draw(f, &mut project))?; - - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') => break, - _ => {} - } - } + terminal.draw(|f| ui::draw(f, &mut state))?; + handle_input(&mut state)?; + if state.quit { break } } // restore terminal diff --git a/src/types.rs b/src/types.rs index c381b64..5b74544 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,7 @@ +use indexmap::IndexMap; use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] pub enum TaskStatus { Done, Todo, @@ -13,7 +14,6 @@ pub enum TaskStatus { pub struct Task { pub title: String, pub description: String, - pub status: TaskStatus, } impl Default for Task { @@ -21,7 +21,6 @@ impl Default for Task { Task { title: String::new(), description: String::new(), - status: TaskStatus::Backlog, } } } @@ -29,16 +28,25 @@ impl Default for Task { #[derive(Deserialize, Serialize, Debug)] pub struct Project { pub name: String, - pub tasks: Vec, + pub tasks: IndexMap>, } impl Project { fn new(name: &str) -> Self { - Project { name: name.to_owned() , tasks: Vec::new() } + Project { + name: name.to_owned(), + tasks: IndexMap::from( + [(TaskStatus::Done, vec![]), + (TaskStatus::Todo, vec![]), + (TaskStatus::InProgress, vec![]), + (TaskStatus::Testing, vec![]), + (TaskStatus::Backlog, vec![])], + ), + } } - fn add_task(&mut self, task: Task) { - self.tasks.push(task); + fn add_task(&mut self, status: TaskStatus, task: Task) { + self.tasks.entry(status).or_default().push(task); } } @@ -46,7 +54,7 @@ impl Default for Project { fn default() -> Self { Project { name: String::new(), - tasks: Vec::new(), + tasks: IndexMap::new(), } } } @@ -60,11 +68,29 @@ impl Project { } /// 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(); } -} \ No newline at end of file +} + +pub struct AppState { + pub selected_column: usize, + pub selected_task: [u8; 5], + pub current_project: Project, + pub quit: bool, +} + +impl AppState { + pub fn new(project: Project) -> Self { + AppState { + selected_column: 0, + selected_task: [0, 0, 0, 0, 0], + quit: false, + current_project: project, + } + } +} + diff --git a/src/ui.rs b/src/ui.rs index d1fc5a3..6a2c139 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,36 +1,47 @@ use tui::backend::{Backend}; -use tui::layout::{Constraint, Direction, Layout, Rect}; +use tui::layout::*; use tui::{Frame}; +use tui::style::{Color, Modifier, Style}; use tui::text::{Span, Spans}; -use tui::widgets::{Block, Borders, List, ListItem, Paragraph}; -use crate::types::{Project, Task}; +use tui::widgets::*; +use crate::types::*; -fn draw_tasks(f: &mut Frame, columns: Vec, tasks: &Vec) { - let ts: Vec = tasks.iter().map(|t| { - ListItem::new(vec![Spans::from(Span::raw(&t.title))]) - }).collect(); +fn draw_tasks(f: &mut Frame, area: &Rect, state: &AppState) { + let columns = Layout::default() + .direction(Direction::Horizontal) + .constraints( + vec![Constraint::Percentage(20); + state.current_project.tasks.len()].as_ref() + ) + .split(*area); - let cols = &["DONE", "TODO", "IN-PROGRESS", "TESTING", "BACKLOG"]; - let blocks: Vec = - columns.iter().enumerate() - .map(|(i, col)| { - Block::default() - .title(cols[i]) - .borders(Borders::ALL) - }).collect(); - let l1 = List::new(ts).block(blocks[0].clone()); - let l2 = List::new(vec![]).block(blocks[1].clone()); - let l3 = List::new(vec![]).block(blocks[2].clone()); - let l4 = List::new(vec![]).block(blocks[3].clone()); - let l5 = List::new(vec![]).block(blocks[4].clone()); - f.render_widget(l1, columns[0]); - f.render_widget(l2, columns[1]); - f.render_widget(l3, columns[2]); - f.render_widget(l4, columns[3]); - f.render_widget(l5, columns[4]); + for (i, (status, tasks)) in state.current_project.tasks.iter().enumerate() { + let items: Vec = tasks.iter().map(|t| { + ListItem::new(vec![Spans::from(Span::raw(&t.title))]) + }).collect(); + let mut style = Style::default(); + if i == state.selected_column { style = style.fg(Color::Green); }; + let mut s = Span::raw(format!("{:?}", status)); + s.style = Style::default() + .add_modifier(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED) + .fg(Color::White); + let block = Block::default() + .style(style) + .title(s) + .borders(Borders::ALL); + let list = List::new(items).block(block); + f.render_widget(list, columns[i]) + } } -pub fn draw(f: &mut Frame, project: &mut Project) { +fn draw_task_info(f: &mut Frame, area: &Rect, state: &AppState) { + let block = Block::default() + .title("TASK INFO") + .borders(Borders::ALL); + f.render_widget(block, *area); +} + +pub fn draw(f: &mut Frame, state: &mut AppState) { let main_layout = Layout::default() .direction(Direction::Vertical) .constraints( @@ -42,37 +53,21 @@ pub fn draw(f: &mut Frame, project: &mut Project) { ].as_ref() ).split(f.size()); - let columns = Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(20), - Constraint::Percentage(20), - ].as_ref() - ) - .split(main_layout[1]); - let block = Block::default() .title("KANBAN BOARD") .borders(Borders::ALL); f.render_widget(block, main_layout[0]); - draw_tasks(f, columns, &project.tasks); + draw_tasks(f, &main_layout[1], &state); + + draw_task_info(f, &main_layout[2], &state); let block = Block::default() - .title("TASK INFO") - .borders(Borders::ALL); - f.render_widget(block, main_layout[2]); - - let block = Block::default() - .title("FOOTER") + .title("KEYBINDINGS") .borders(Borders::ALL); let foot_txt = - Paragraph::new("Press 'q' to quit") + Paragraph::new("q : Quit | ⏪🔽🔼⏩ or hjkl : Navigation") .block(block); f.render_widget(foot_txt, main_layout[3]); } \ No newline at end of file