Rendering tasks to their individual columns
This commit is contained in:
parent
60e4ba0770
commit
436c9ed3cf
18
Cargo.lock
generated
18
Cargo.lock
generated
@ -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",
|
||||
|
@ -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" ] }
|
22
src/input.rs
Normal file
22
src/input.rs
Normal 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(())
|
||||
}
|
23
src/main.rs
23
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
|
||||
|
46
src/types.rs
46
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<Task>,
|
||||
pub tasks: IndexMap<TaskStatus, Vec<Task>>,
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
89
src/ui.rs
89
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<B: Backend>(f: &mut Frame<B>, columns: Vec<Rect>, tasks: &Vec<Task>) {
|
||||
let ts: Vec<ListItem> = tasks.iter().map(|t| {
|
||||
ListItem::new(vec![Spans::from(Span::raw(&t.title))])
|
||||
}).collect();
|
||||
fn draw_tasks<B: Backend>(f: &mut Frame<B>, 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<Block> =
|
||||
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<ListItem> = 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<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()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
@ -42,37 +53,21 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, 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]);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user