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",
|
"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",
|
||||||
|
@ -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
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 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
|
||||||
|
46
src/types.rs
46
src/types.rs
@ -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,11 +68,29 @@ 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());
|
||||||
// let json = serde_json::to_string_pretty(&project).unwrap();
|
// let json = serde_json::to_string_pretty(&project).unwrap();
|
||||||
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
89
src/ui.rs
89
src/ui.rs
@ -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]);
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user