Rendering tasks to their individual columns

This commit is contained in:
Joseph Ferano 2022-12-04 02:47:37 +04:00
parent 60e4ba0770
commit 436c9ed3cf
6 changed files with 128 additions and 71 deletions

18
Cargo.lock generated
View File

@ -51,6 +51,23 @@ dependencies = [
"winapi", "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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.4" version = "1.0.4"
@ -62,6 +79,7 @@ name = "kanban-tui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"crossterm", "crossterm",
"indexmap",
"serde", "serde",
"serde_json", "serde_json",
"tui", "tui",

View File

@ -10,3 +10,4 @@ tui = "0.19.0"
crossterm = "0.25" crossterm = "0.25"
serde = { version = "1.0.148" , features = [ "derive" ] } serde = { version = "1.0.148" , features = [ "derive" ] }
serde_json = "1.0.89" serde_json = "1.0.89"
indexmap = { version = "1.9.2" , features = [ "serde" ] }

22
src/input.rs Normal file
View File

@ -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(())
}

View File

@ -1,14 +1,14 @@
#![allow(dead_code)]
mod ui; mod ui;
mod types; mod types;
mod input;
use std::{io}; use std::{io};
use crossterm::{ use crossterm::{event::*, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}};
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}
};
use tui::backend::CrosstermBackend; use tui::backend::CrosstermBackend;
use tui::Terminal; use tui::Terminal;
use crate::types::{Project, Task}; use crate::input::handle_input;
use crate::types::*;
fn main() -> Result<(), io::Error> { fn main() -> Result<(), io::Error> {
// setup terminal // setup terminal
@ -18,17 +18,12 @@ fn main() -> Result<(), io::Error> {
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
let mut project = Project::load(); let mut state = AppState::new(Project::load());
loop { loop {
terminal.draw(|f| ui::draw(f, &mut project))?; terminal.draw(|f| ui::draw(f, &mut state))?;
handle_input(&mut state)?;
if let Event::Key(key) = event::read()? { if state.quit { break }
match key.code {
KeyCode::Char('q') => break,
_ => {}
}
}
} }
// restore terminal // restore terminal

View File

@ -1,6 +1,7 @@
use indexmap::IndexMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum TaskStatus { pub enum TaskStatus {
Done, Done,
Todo, Todo,
@ -13,7 +14,6 @@ pub enum TaskStatus {
pub struct Task { pub struct Task {
pub title: String, pub title: String,
pub description: String, pub description: String,
pub status: TaskStatus,
} }
impl Default for Task { impl Default for Task {
@ -21,7 +21,6 @@ impl Default for Task {
Task { Task {
title: String::new(), title: String::new(),
description: String::new(), description: String::new(),
status: TaskStatus::Backlog,
} }
} }
} }
@ -29,16 +28,25 @@ impl Default for Task {
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
pub struct Project { pub struct Project {
pub name: String, pub name: String,
pub tasks: Vec<Task>, pub tasks: IndexMap<TaskStatus, Vec<Task>>,
} }
impl Project { impl Project {
fn new(name: &str) -> Self { 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) { fn add_task(&mut self, status: TaskStatus, task: Task) {
self.tasks.push(task); self.tasks.entry(status).or_default().push(task);
} }
} }
@ -46,7 +54,7 @@ impl Default for Project {
fn default() -> Self { fn default() -> Self {
Project { Project {
name: String::new(), name: String::new(),
tasks: Vec::new(), tasks: IndexMap::new(),
} }
} }
} }
@ -60,7 +68,6 @@ impl Project {
} }
/// Comment out cause this is dangerous /// Comment out cause this is dangerous
pub fn save() { pub fn save() {
// let mut project = Project::new("Kanban Tui"); // let mut project = Project::new("Kanban Tui");
// project.add_task(Task::default()); // project.add_task(Task::default());
// project.add_task(Task::default()); // project.add_task(Task::default());
@ -68,3 +75,22 @@ impl Project {
// std::fs::write("./project.json", json).unwrap(); // std::fs::write("./project.json", json).unwrap();
} }
} }
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,
}
}
}

View File

@ -1,36 +1,47 @@
use tui::backend::{Backend}; use tui::backend::{Backend};
use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::layout::*;
use tui::{Frame}; use tui::{Frame};
use tui::style::{Color, Modifier, Style};
use tui::text::{Span, Spans}; use tui::text::{Span, Spans};
use tui::widgets::{Block, Borders, List, ListItem, Paragraph}; use tui::widgets::*;
use crate::types::{Project, Task}; use crate::types::*;
fn draw_tasks<B: Backend>(f: &mut Frame<B>, columns: Vec<Rect>, tasks: &Vec<Task>) { fn draw_tasks<B: Backend>(f: &mut Frame<B>, area: &Rect, state: &AppState) {
let ts: Vec<ListItem> = tasks.iter().map(|t| { let columns = Layout::default()
ListItem::new(vec![Spans::from(Span::raw(&t.title))]) .direction(Direction::Horizontal)
}).collect(); .constraints(
vec![Constraint::Percentage(20);
state.current_project.tasks.len()].as_ref()
)
.split(*area);
let cols = &["DONE", "TODO", "IN-PROGRESS", "TESTING", "BACKLOG"]; for (i, (status, tasks)) in state.current_project.tasks.iter().enumerate() {
let blocks: Vec<Block> = let items: Vec<ListItem> = tasks.iter().map(|t| {
columns.iter().enumerate() ListItem::new(vec![Spans::from(Span::raw(&t.title))])
.map(|(i, col)| { }).collect();
Block::default() let mut style = Style::default();
.title(cols[i]) if i == state.selected_column { style = style.fg(Color::Green); };
.borders(Borders::ALL) let mut s = Span::raw(format!("{:?}", status));
}).collect(); s.style = Style::default()
let l1 = List::new(ts).block(blocks[0].clone()); .add_modifier(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED)
let l2 = List::new(vec![]).block(blocks[1].clone()); .fg(Color::White);
let l3 = List::new(vec![]).block(blocks[2].clone()); let block = Block::default()
let l4 = List::new(vec![]).block(blocks[3].clone()); .style(style)
let l5 = List::new(vec![]).block(blocks[4].clone()); .title(s)
f.render_widget(l1, columns[0]); .borders(Borders::ALL);
f.render_widget(l2, columns[1]); let list = List::new(items).block(block);
f.render_widget(l3, columns[2]); f.render_widget(list, columns[i])
f.render_widget(l4, columns[3]); }
f.render_widget(l5, columns[4]);
} }
pub fn draw<B: Backend>(f: &mut Frame<B>, project: &mut Project) { fn draw_task_info<B: Backend>(f: &mut Frame<B>, area: &Rect, state: &AppState) {
let block = Block::default()
.title("TASK INFO")
.borders(Borders::ALL);
f.render_widget(block, *area);
}
pub fn draw<B: Backend>(f: &mut Frame<B>, state: &mut AppState) {
let main_layout = Layout::default() let main_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints( .constraints(
@ -42,37 +53,21 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, project: &mut Project) {
].as_ref() ].as_ref()
).split(f.size()); ).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() let block = Block::default()
.title("KANBAN BOARD") .title("KANBAN BOARD")
.borders(Borders::ALL); .borders(Borders::ALL);
f.render_widget(block, main_layout[0]); 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() let block = Block::default()
.title("TASK INFO") .title("KEYBINDINGS")
.borders(Borders::ALL);
f.render_widget(block, main_layout[2]);
let block = Block::default()
.title("FOOTER")
.borders(Borders::ALL); .borders(Borders::ALL);
let foot_txt = let foot_txt =
Paragraph::new("Press 'q' to quit") Paragraph::new("q : Quit | ⏪🔽🔼⏩ or hjkl : Navigation")
.block(block); .block(block);
f.render_widget(foot_txt, main_layout[3]); f.render_widget(foot_txt, main_layout[3]);
} }