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