Added task shifting up and down and left and right.
Francesco added some stuff as well.
This commit is contained in:
parent
c38fa823c9
commit
ae99fa193c
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -2,6 +2,12 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.66"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -99,11 +105,13 @@ checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
|
|||||||
name = "kanban-tui"
|
name = "kanban-tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"int-enum",
|
"int-enum",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
"tui",
|
"tui",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -12,3 +12,7 @@ serde = { version = "1.0.148" , features = [ "derive" ] }
|
|||||||
serde_json = "1.0.89"
|
serde_json = "1.0.89"
|
||||||
indexmap = { version = "1.9.2" , features = [ "serde" ] }
|
indexmap = { version = "1.9.2" , features = [ "serde" ] }
|
||||||
int-enum = "0.5.0"
|
int-enum = "0.5.0"
|
||||||
|
|
||||||
|
#error handling
|
||||||
|
thiserror = "1"
|
||||||
|
anyhow = "1"
|
198
src/app.rs
Normal file
198
src/app.rs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
use std::cmp::min;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use int_enum::IntEnum;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
#[repr(usize)]
|
||||||
|
#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, IntEnum)]
|
||||||
|
pub enum TaskStatus {
|
||||||
|
Todo = 0,
|
||||||
|
InProgress = 1,
|
||||||
|
Done = 2,
|
||||||
|
Ideas = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Deserialize, Serialize, Debug, Clone, Copy)]
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Task {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Task {
|
||||||
|
fn default() -> Self {
|
||||||
|
Task {
|
||||||
|
title: String::new(),
|
||||||
|
description: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type used mainly for serialization at this time
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
pub struct Project {
|
||||||
|
pub name: String,
|
||||||
|
pub tasks_per_column: IndexMap<TaskStatus, Vec<Task>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum KanbanError {
|
||||||
|
#[error("There is something wrong with the json schema, it doesn't match Project struct")]
|
||||||
|
BadJson,
|
||||||
|
#[error("Some form of IO error occured: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Project {
|
||||||
|
pub fn new(name: &str) -> Self {
|
||||||
|
Project {
|
||||||
|
name: name.to_owned(),
|
||||||
|
tasks_per_column: IndexMap::from(
|
||||||
|
[(TaskStatus::Done, vec![]),
|
||||||
|
(TaskStatus::Todo, vec![]),
|
||||||
|
(TaskStatus::InProgress, vec![]),
|
||||||
|
(TaskStatus::Ideas, vec![])],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_from_json(json: &str) -> Result<Self, KanbanError> {
|
||||||
|
serde_json::from_str(json).map_err(|_| KanbanError::BadJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Result<Self, KanbanError> {
|
||||||
|
let json = std::fs::read_to_string("kanban-tui.json")?;
|
||||||
|
Self::load_from_json(&json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_task(&mut self, status: TaskStatus, task: Task) {
|
||||||
|
self.tasks_per_column.entry(status).or_default().push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Project {
|
||||||
|
fn default() -> Self {
|
||||||
|
Project {
|
||||||
|
name: String::new(),
|
||||||
|
tasks_per_column: IndexMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub selected_column: usize,
|
||||||
|
pub selected_task: [usize; 4],
|
||||||
|
pub project: Project,
|
||||||
|
pub quit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(project: Project) -> Self {
|
||||||
|
AppState {
|
||||||
|
selected_column: 0,
|
||||||
|
selected_task: [0, 0, 0, 0],
|
||||||
|
quit: false,
|
||||||
|
project,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_task_idx(&self) -> usize {
|
||||||
|
self.selected_task[self.selected_column]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_task_idx_mut(&mut self) -> &mut usize {
|
||||||
|
&mut self.selected_task[self.selected_column]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tasks_in_active_column(&self) -> &[Task] {
|
||||||
|
let column: TaskStatus = TaskStatus::from_int(self.selected_column).unwrap().clone();
|
||||||
|
self.project.tasks_per_column.get(&column).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tasks_in_active_column_mut(&mut self) -> &mut Vec<Task> {
|
||||||
|
let column: TaskStatus = TaskStatus::from_int(self.selected_column).unwrap().clone();
|
||||||
|
self.project.tasks_per_column.get_mut(&column).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_task(&self) -> Option<&Task> {
|
||||||
|
let tasks = self.get_tasks_in_active_column();
|
||||||
|
tasks.get(self.selected_task_idx())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous_task(&mut self) {
|
||||||
|
*self.selected_task_idx_mut() = self.selected_task_idx().saturating_sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next_task(&mut self) {
|
||||||
|
let tasks = self.get_tasks_in_active_column();
|
||||||
|
if tasks.len() > 0 {
|
||||||
|
let mins = min(self.selected_task_idx() + 1, tasks.len() - 1);
|
||||||
|
*self.selected_task_idx_mut() = mins;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous_column(&mut self) {
|
||||||
|
self.selected_column = self.selected_column.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next_column(&mut self) {
|
||||||
|
self.selected_column = min(self.selected_column + 1, self.project.tasks_per_column.len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_task_previous_column(&mut self) {
|
||||||
|
let tasks = self.get_tasks_in_active_column();
|
||||||
|
let task_idx = self.selected_task_idx();
|
||||||
|
if self.selected_column > 0 && tasks.len() > 0 && task_idx.clone() < tasks.len() {
|
||||||
|
let task = self.get_tasks_in_active_column_mut().remove(task_idx);
|
||||||
|
*self.selected_task_idx_mut() = self.selected_task_idx().saturating_sub(1);
|
||||||
|
self.select_previous_column();
|
||||||
|
let target_tasks = self.get_tasks_in_active_column_mut();
|
||||||
|
target_tasks.push(task);
|
||||||
|
*self.selected_task_idx_mut() = target_tasks.len() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_task_next_column(&mut self) {
|
||||||
|
let tasks = self.get_tasks_in_active_column();
|
||||||
|
let task_idx = self.selected_task_idx();
|
||||||
|
if self.selected_column < self.project.tasks_per_column.len() && tasks.len() > 0 && task_idx < tasks.len() {
|
||||||
|
let task = self.get_tasks_in_active_column_mut().remove(task_idx);
|
||||||
|
*self.selected_task_idx_mut() = self.selected_task_idx().saturating_sub(1);
|
||||||
|
self.select_next_column();
|
||||||
|
let target_tasks = self.get_tasks_in_active_column_mut();
|
||||||
|
target_tasks.push(task);
|
||||||
|
*self.selected_task_idx_mut() = target_tasks.len() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_task_up(&mut self) {
|
||||||
|
let task_idx = self.selected_task_idx();
|
||||||
|
if task_idx > 0 {
|
||||||
|
let tasks = self.get_tasks_in_active_column_mut();
|
||||||
|
tasks.swap(task_idx, task_idx - 1);
|
||||||
|
*self.selected_task_idx_mut() = task_idx - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_task_down(&mut self) {
|
||||||
|
let task_idx = self.selected_task_idx();
|
||||||
|
let tasks = self.get_tasks_in_active_column_mut();
|
||||||
|
if task_idx < tasks.len() - 1 {
|
||||||
|
tasks.swap(task_idx, task_idx + 1);
|
||||||
|
*self.selected_task_idx_mut() = task_idx + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
1
src/app/tests.rs
Normal file
1
src/app/tests.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
use super::*;
|
10
src/input.rs
10
src/input.rs
@ -1,6 +1,6 @@
|
|||||||
use crossterm::event;
|
use crossterm::event;
|
||||||
use crossterm::event::{Event, KeyCode};
|
use crossterm::event::{Event, KeyCode};
|
||||||
use crate::types::{AppState};
|
use crate::app::{AppState};
|
||||||
|
|
||||||
pub fn handle_input(state: &mut AppState) -> Result<(), std::io::Error> {
|
pub fn handle_input(state: &mut AppState) -> Result<(), std::io::Error> {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
@ -14,6 +14,14 @@ pub fn handle_input(state: &mut AppState) -> Result<(), std::io::Error> {
|
|||||||
KeyCode::Up => state.select_previous_task(),
|
KeyCode::Up => state.select_previous_task(),
|
||||||
KeyCode::Char('l') |
|
KeyCode::Char('l') |
|
||||||
KeyCode::Right => state.select_next_column(),
|
KeyCode::Right => state.select_next_column(),
|
||||||
|
KeyCode::Char('<') |
|
||||||
|
KeyCode::Char('H') => state.move_task_previous_column(),
|
||||||
|
KeyCode::Char('>') |
|
||||||
|
KeyCode::Char('L') => state.move_task_next_column(),
|
||||||
|
KeyCode::Char('=') |
|
||||||
|
KeyCode::Char('J') => state.move_task_down(),
|
||||||
|
KeyCode::Char('-') |
|
||||||
|
KeyCode::Char('K') => state.move_task_up(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
src/lib.rs
Normal file
7
src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
mod app;
|
||||||
|
mod ui;
|
||||||
|
mod input;
|
||||||
|
|
||||||
|
pub use app::*;
|
||||||
|
pub use ui::draw;
|
||||||
|
pub use input::handle_input;
|
18
src/main.rs
18
src/main.rs
@ -1,29 +1,23 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
mod ui;
|
use kanban_tui::{AppState, Project};
|
||||||
mod types;
|
|
||||||
mod input;
|
|
||||||
|
|
||||||
use std::{io};
|
use std::{io};
|
||||||
use crossterm::{event::*, 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::backend::CrosstermBackend;
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
use crate::input::handle_input;
|
|
||||||
use crate::types::*;
|
|
||||||
|
|
||||||
fn main() -> Result<(), io::Error> {
|
fn main() -> anyhow::Result<()> {
|
||||||
// setup terminal
|
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
crossterm::execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
let backend = CrosstermBackend::new(stdout);
|
let backend = CrosstermBackend::new(stdout);
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
let mut state = AppState::new(Project::load());
|
let mut state = AppState::new(Project::load()?);
|
||||||
|
|
||||||
loop {
|
while !state.quit {
|
||||||
terminal.draw(|f| ui::draw(f, &mut state))?;
|
terminal.draw(|f| kanban_tui::draw(f, &mut state))?;
|
||||||
handle_input(&mut state)?;
|
kanban_tui::handle_input(&mut state)?;
|
||||||
if state.quit { break }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore terminal
|
// restore terminal
|
||||||
|
123
src/types.rs
123
src/types.rs
@ -1,123 +0,0 @@
|
|||||||
use std::cmp::min;
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use int_enum::IntEnum;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[repr(usize)]
|
|
||||||
#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, IntEnum)]
|
|
||||||
pub enum TaskStatus {
|
|
||||||
Todo = 0,
|
|
||||||
InProgress = 1,
|
|
||||||
Done = 2,
|
|
||||||
Ideas = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[derive(Deserialize, Serialize, Debug, Clone, Copy)]
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
|
||||||
pub struct Task {
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Task {
|
|
||||||
fn default() -> Self {
|
|
||||||
Task {
|
|
||||||
title: String::new(),
|
|
||||||
description: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Type used mainly for serialization at this time
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
|
||||||
pub struct Project {
|
|
||||||
pub name: String,
|
|
||||||
pub tasks_per_column: IndexMap<TaskStatus, Vec<Task>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Project {
|
|
||||||
fn new(name: &str) -> Self {
|
|
||||||
Project {
|
|
||||||
name: name.to_owned(),
|
|
||||||
tasks_per_column: IndexMap::from(
|
|
||||||
[(TaskStatus::Done, vec![]),
|
|
||||||
(TaskStatus::Todo, vec![]),
|
|
||||||
(TaskStatus::InProgress, vec![]),
|
|
||||||
(TaskStatus::Ideas, vec![])],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load() -> Self {
|
|
||||||
let json = std::fs::read_to_string("kanban-tui.json")
|
|
||||||
.expect("Could not read json file");
|
|
||||||
serde_json::from_str(&json)
|
|
||||||
.expect("There is something wrong with the json schema, it doesn't match Project struct")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_task(&mut self, status: TaskStatus, task: Task) {
|
|
||||||
self.tasks_per_column.entry(status).or_default().push(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Project {
|
|
||||||
fn default() -> Self {
|
|
||||||
Project {
|
|
||||||
name: String::new(),
|
|
||||||
tasks_per_column: IndexMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AppState {
|
|
||||||
pub selected_column: usize,
|
|
||||||
pub selected_task: [usize; 4],
|
|
||||||
pub project: Project,
|
|
||||||
pub quit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppState {
|
|
||||||
pub fn new(project: Project) -> Self {
|
|
||||||
AppState {
|
|
||||||
selected_column: 0,
|
|
||||||
selected_task: [0, 0, 0, 0],
|
|
||||||
quit: false,
|
|
||||||
project: project,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_tasks_in_active_column(&self) -> &Vec<Task> {
|
|
||||||
let column: TaskStatus = TaskStatus::from_int(self.selected_column).unwrap().clone();
|
|
||||||
self.project.tasks_per_column.get(&column).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_previous_task(&mut self) {
|
|
||||||
self.selected_task[self.selected_column] = self.selected_task[self.selected_column].saturating_sub(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_next_task(&mut self) {
|
|
||||||
let tasks = self.get_tasks_in_active_column();
|
|
||||||
if tasks.len() > 0 {
|
|
||||||
let mins = min(self.selected_task[self.selected_column] + 1, tasks.len() - 1);
|
|
||||||
self.selected_task[self.selected_column] = mins;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_previous_column(&mut self) {
|
|
||||||
self.selected_column = self.selected_column.saturating_sub(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select_next_column(&mut self) {
|
|
||||||
self.selected_column = min(self.selected_column + 1, self.project.tasks_per_column.len() - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -4,8 +4,7 @@ use tui::{Frame};
|
|||||||
use tui::style::{Color, Modifier, Style};
|
use tui::style::{Color, Modifier, Style};
|
||||||
use tui::text::{Span, Spans};
|
use tui::text::{Span, Spans};
|
||||||
use tui::widgets::*;
|
use tui::widgets::*;
|
||||||
use crate::types::*;
|
use crate::app::*;
|
||||||
use int_enum::IntEnum;
|
|
||||||
|
|
||||||
fn draw_tasks<B: Backend>(f: &mut Frame<B>, area: &Rect, state: &AppState) {
|
fn draw_tasks<B: Backend>(f: &mut Frame<B>, area: &Rect, state: &AppState) {
|
||||||
let columns = Layout::default()
|
let columns = Layout::default()
|
||||||
@ -47,10 +46,7 @@ fn draw_task_info<B: Backend>(f: &mut Frame<B>, area: &Rect, state: &AppState) {
|
|||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.title("TASK INFO")
|
.title("TASK INFO")
|
||||||
.borders(Borders::ALL);
|
.borders(Borders::ALL);
|
||||||
let column: TaskStatus = TaskStatus::from_int(state.selected_column).unwrap();
|
if let Some(task) = state.get_selected_task() {
|
||||||
let tasks = state.project.tasks_per_column.get(&column).unwrap();
|
|
||||||
if tasks.len() > 0 {
|
|
||||||
let task: &Task = &tasks[state.selected_task[state.selected_column]];
|
|
||||||
let p = Paragraph::new(task.description.as_str()).block(block).wrap(Wrap { trim: true });
|
let p = Paragraph::new(task.description.as_str()).block(block).wrap(Wrap { trim: true });
|
||||||
f.render_widget(p, *area);
|
f.render_widget(p, *area);
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user