Compare commits
10 Commits
52097aed09
...
ba4a98af56
Author | SHA1 | Date | |
---|---|---|---|
ba4a98af56 | |||
5ba313c568 | |||
5c11808e7d | |||
50156e1e01 | |||
d3a9679910 | |||
001652a63a | |||
7889c2358c | |||
c999764a28 | |||
9b4341ccf1 | |||
2e2145b2ab |
68
Cargo.lock
generated
68
Cargo.lock
generated
@ -2,6 +2,15 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "0.7.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
@ -26,6 +35,12 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.0.78"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@ -57,23 +72,6 @@ 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 = "int-enum"
|
name = "int-enum"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -106,12 +104,13 @@ name = "kanban-tui"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"cc",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"indexmap",
|
|
||||||
"int-enum",
|
"int-enum",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"tree-sitter",
|
||||||
"tui",
|
"tui",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -140,6 +139,12 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@ -219,6 +224,23 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
@ -338,6 +360,16 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tree-sitter"
|
||||||
|
version = "0.20.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4423c784fe11398ca91e505cdc71356b07b1a924fc8735cfab5333afe3e18bc"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tui"
|
name = "tui"
|
||||||
version = "0.19.0"
|
version = "0.19.0"
|
||||||
|
@ -10,9 +10,10 @@ 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" ] }
|
|
||||||
int-enum = "0.5.0"
|
int-enum = "0.5.0"
|
||||||
|
|
||||||
#error handling
|
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
tree-sitter = "0.20.9"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc="*"
|
||||||
|
253
src/app.rs
253
src/app.rs
@ -1,18 +1,16 @@
|
|||||||
use std::cmp::min;
|
// use indexmap::IndexMap;
|
||||||
use indexmap::IndexMap;
|
// use int_enum::IntEnum;
|
||||||
use int_enum::IntEnum;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::cmp::min;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
#[repr(usize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[derive(Deserialize, Serialize, Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, IntEnum)]
|
pub struct Column {
|
||||||
pub enum TaskStatus {
|
pub name: String,
|
||||||
Todo = 0,
|
pub selected_task_idx: usize,
|
||||||
InProgress = 1,
|
pub tasks: Vec<Task>,
|
||||||
Done = 2,
|
|
||||||
Ideas = 3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[derive(Deserialize, Serialize, Debug, Clone, Copy)]
|
// #[derive(Deserialize, Serialize, Debug, Clone, Copy)]
|
||||||
@ -35,7 +33,8 @@ 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_per_column: IndexMap<TaskStatus, Vec<Task>>,
|
pub selected_column_idx: usize,
|
||||||
|
pub columns: Vec<Column>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
@ -46,16 +45,74 @@ pub enum KanbanError {
|
|||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Project {
|
||||||
|
fn default() -> Self {
|
||||||
|
Project {
|
||||||
|
name: String::new(),
|
||||||
|
columns: vec![],
|
||||||
|
selected_column_idx: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub project: Project,
|
||||||
|
pub quit: bool,
|
||||||
|
pub columns: Vec<Column>,
|
||||||
|
pub popup_text: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(project: Project) -> Self {
|
||||||
|
AppState {
|
||||||
|
quit: false,
|
||||||
|
popup_text: None,
|
||||||
|
project,
|
||||||
|
columns: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Column {
|
||||||
|
pub fn new(name: &str) -> Self {
|
||||||
|
Column {
|
||||||
|
name: name.to_owned(),
|
||||||
|
tasks: vec![],
|
||||||
|
selected_task_idx: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_task(&self) -> Option<&Task> {
|
||||||
|
self.tasks.get(self.selected_task_idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_task_mut(&mut self) -> Option<&mut Task> {
|
||||||
|
self.tasks.get_mut(self.selected_task_idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous_task(&mut self) {
|
||||||
|
let task_idx = &mut self.selected_task_idx;
|
||||||
|
*task_idx = task_idx.saturating_sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next_task(&mut self) {
|
||||||
|
let task_idx = &mut self.selected_task_idx;
|
||||||
|
*task_idx = min(*task_idx + 1, self.tasks.len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_last_task(&mut self) {
|
||||||
|
self.selected_task_idx = self.tasks.len() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
impl Project {
|
impl Project {
|
||||||
pub fn new(name: &str) -> Self {
|
pub fn new(name: &str) -> Self {
|
||||||
Project {
|
Project {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
tasks_per_column: IndexMap::from(
|
columns: vec![],
|
||||||
[(TaskStatus::Done, vec![]),
|
selected_column_idx: 0,
|
||||||
(TaskStatus::Todo, vec![]),
|
|
||||||
(TaskStatus::InProgress, vec![]),
|
|
||||||
(TaskStatus::Ideas, vec![])],
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,131 +125,83 @@ impl Project {
|
|||||||
Self::load_from_json(&json)
|
Self::load_from_json(&json)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_task(&mut self, status: TaskStatus, task: Task) {
|
// pub fn add_task(&mut self, status: Column, task: Task) {
|
||||||
self.tasks_per_column.entry(status).or_default().push(task);
|
// self.tasks_per_column.entry(status).or_default().push(task);
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub fn save(&self) {
|
pub fn save(&self) {
|
||||||
let json = serde_json::to_string_pretty(&self).unwrap();
|
let json = serde_json::to_string_pretty(&self).unwrap();
|
||||||
std::fs::write("kanban-tui.json", json).unwrap();
|
std::fs::write("kanban-tui.json", json).unwrap();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Project {
|
pub fn get_selected_column(&self) -> &Column {
|
||||||
fn default() -> Self {
|
&self.columns[self.selected_column_idx]
|
||||||
Project {
|
}
|
||||||
name: String::new(),
|
|
||||||
tasks_per_column: IndexMap::new(),
|
pub fn get_selected_column_mut(&mut self) -> &mut Column {
|
||||||
|
&mut self.columns[self.selected_column_idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_previous_column(&mut self) -> &Column {
|
||||||
|
self.selected_column_idx = self.selected_column_idx.saturating_sub(1);
|
||||||
|
&self.columns[self.selected_column_idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_next_column(&mut self) -> &Column {
|
||||||
|
self.selected_column_idx = min(
|
||||||
|
self.selected_column_idx + 1,
|
||||||
|
self.columns.len() - 1,
|
||||||
|
);
|
||||||
|
&self.columns[self.selected_column_idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_task_to_column(&mut self, move_next: bool) {
|
||||||
|
let col_idx = self.selected_column_idx;
|
||||||
|
let cols_len = self.columns.len();
|
||||||
|
let column = self.get_selected_column_mut();
|
||||||
|
let cond = if move_next {
|
||||||
|
col_idx < cols_len - 1
|
||||||
|
} else {
|
||||||
|
col_idx > 0
|
||||||
|
};
|
||||||
|
if cond && column.tasks.len() > 0 {
|
||||||
|
let t = column.tasks.remove(column.selected_task_idx);
|
||||||
|
column.select_previous_task();
|
||||||
|
if move_next {
|
||||||
|
self.select_next_column();
|
||||||
|
} else {
|
||||||
|
self.select_previous_column();
|
||||||
|
}
|
||||||
|
let col = self.get_selected_column_mut();
|
||||||
|
col.tasks.push(t);
|
||||||
|
col.select_last_task();
|
||||||
|
self.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
pub fn move_task_previous_column(&mut self) {
|
||||||
let tasks = self.get_tasks_in_active_column();
|
self.move_task_to_column(false)
|
||||||
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;
|
|
||||||
self.project.save();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_task_next_column(&mut self) {
|
pub fn move_task_next_column(&mut self) {
|
||||||
let tasks = self.get_tasks_in_active_column();
|
self.move_task_to_column(true)
|
||||||
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;
|
|
||||||
self.project.save();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_task_up(&mut self) {
|
pub fn move_task_up(&mut self) {
|
||||||
let task_idx = self.selected_task_idx();
|
let column = self.get_selected_column_mut();
|
||||||
if task_idx > 0 {
|
if column.selected_task_idx > 0 {
|
||||||
let tasks = self.get_tasks_in_active_column_mut();
|
column.tasks.swap(column.selected_task_idx, column.selected_task_idx - 1);
|
||||||
tasks.swap(task_idx, task_idx - 1);
|
column.selected_task_idx = column.selected_task_idx - 1;
|
||||||
*self.selected_task_idx_mut() = task_idx - 1;
|
self.save();
|
||||||
self.project.save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_task_down(&mut self) {
|
pub fn move_task_down(&mut self) {
|
||||||
let task_idx = self.selected_task_idx();
|
let column = self.get_selected_column_mut();
|
||||||
let tasks = self.get_tasks_in_active_column_mut();
|
if column.selected_task_idx < column.tasks.len() - 1 {
|
||||||
if task_idx < tasks.len() - 1 {
|
column.tasks.swap(column.selected_task_idx, column.selected_task_idx + 1);
|
||||||
tasks.swap(task_idx, task_idx + 1);
|
column.selected_task_idx = column.selected_task_idx + 1;
|
||||||
*self.selected_task_idx_mut() = task_idx + 1;
|
self.save();
|
||||||
self.project.save();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
24
src/input.rs
24
src/input.rs
@ -3,25 +3,33 @@ use crossterm::event::{Event, KeyCode};
|
|||||||
use crate::app::{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> {
|
||||||
|
let project = &mut state.project;
|
||||||
|
let column = project.get_selected_column_mut();
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => state.quit = true,
|
KeyCode::Char('q') => state.quit = true,
|
||||||
KeyCode::Char('h') |
|
KeyCode::Char('h') |
|
||||||
KeyCode::Left => state.select_previous_column(),
|
KeyCode::Left => { project.select_previous_column(); },
|
||||||
KeyCode::Char('j') |
|
KeyCode::Char('j') |
|
||||||
KeyCode::Down => state.select_next_task(),
|
KeyCode::Down => column.select_next_task(),
|
||||||
KeyCode::Char('k') |
|
KeyCode::Char('k') |
|
||||||
KeyCode::Up => state.select_previous_task(),
|
KeyCode::Up => column.select_previous_task(),
|
||||||
KeyCode::Char('l') |
|
KeyCode::Char('l') |
|
||||||
KeyCode::Right => state.select_next_column(),
|
KeyCode::Right => { project.select_next_column(); },
|
||||||
KeyCode::Char('<') |
|
KeyCode::Char('<') |
|
||||||
KeyCode::Char('H') => state.move_task_previous_column(),
|
KeyCode::Char('H') => project.move_task_previous_column(),
|
||||||
KeyCode::Char('>') |
|
KeyCode::Char('>') |
|
||||||
KeyCode::Char('L') => state.move_task_next_column(),
|
KeyCode::Char('L') => project.move_task_next_column(),
|
||||||
KeyCode::Char('=') |
|
KeyCode::Char('=') |
|
||||||
KeyCode::Char('J') => state.move_task_down(),
|
KeyCode::Char('J') => project.move_task_down(),
|
||||||
KeyCode::Char('-') |
|
KeyCode::Char('-') |
|
||||||
KeyCode::Char('K') => state.move_task_up(),
|
KeyCode::Char('K') => project.move_task_up(),
|
||||||
|
KeyCode::Char('p') => {
|
||||||
|
match state.popup_text {
|
||||||
|
None => state.popup_text = Some("".to_string()),
|
||||||
|
Some(_) => state.popup_text = None,
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod treesitter;
|
||||||
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use ui::draw;
|
pub use ui::draw;
|
||||||
pub use input::handle_input;
|
pub use input::handle_input;
|
||||||
|
pub use treesitter::compile_md_grammar;
|
||||||
|
11
src/main.rs
11
src/main.rs
@ -1,8 +1,11 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
use kanban_tui::{AppState, Project};
|
use kanban_tui::*;
|
||||||
|
|
||||||
use std::{io};
|
use crossterm::{
|
||||||
use crossterm::{event::*, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}};
|
event::*,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use std::io;
|
||||||
use tui::backend::CrosstermBackend;
|
use tui::backend::CrosstermBackend;
|
||||||
use tui::Terminal;
|
use tui::Terminal;
|
||||||
|
|
||||||
@ -20,6 +23,8 @@ fn main() -> anyhow::Result<()> {
|
|||||||
kanban_tui::handle_input(&mut state)?;
|
kanban_tui::handle_input(&mut state)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.project.save();
|
||||||
|
|
||||||
// restore terminal
|
// restore terminal
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
crossterm::execute!(
|
crossterm::execute!(
|
||||||
|
6
src/treesitter.rs
Normal file
6
src/treesitter.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use tree_sitter::{Parser, Language};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn compile_md_grammar() {
|
||||||
|
}
|
||||||
|
|
154
src/ui.rs
154
src/ui.rs
@ -1,59 +1,133 @@
|
|||||||
use tui::backend::{Backend};
|
use crate::app::*;
|
||||||
|
use tui::backend::Backend;
|
||||||
use tui::layout::*;
|
use tui::layout::*;
|
||||||
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::app::*;
|
use tui::Frame;
|
||||||
|
|
||||||
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()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
.constraints(
|
.constraints(
|
||||||
vec![Constraint::Percentage(100 / state.project.tasks_per_column.len() as u16);
|
vec![
|
||||||
state.project.tasks_per_column.len()].as_ref()
|
Constraint::Percentage(100 / state.project.columns.len() as u16);
|
||||||
|
state.project.columns.len()
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.split(*area);
|
.split(*area);
|
||||||
|
|
||||||
for (i, (status, tasks)) in state.project.tasks_per_column.iter().enumerate() {
|
for (i, column) in state.project.columns.iter().enumerate() {
|
||||||
let items: Vec<ListItem> = tasks.iter().enumerate().map(|(j, task)| {
|
let items: Vec<ListItem> = column.tasks
|
||||||
let mut style = Style::default();
|
.iter()
|
||||||
if i == state.selected_column && j == state.selected_task[state.selected_column] {
|
.enumerate()
|
||||||
style = style.fg(Color::White).add_modifier(Modifier::BOLD);
|
.map(|(j, task)| {
|
||||||
} else {
|
let mut style = Style::default();
|
||||||
style = style.fg(Color::White);
|
let col_idx = state.project.selected_column_idx;
|
||||||
}
|
let task_idx = state.project.get_selected_column().selected_task_idx;
|
||||||
let mut s = Span::raw(task.title.as_str());
|
if i == col_idx && j == task_idx {
|
||||||
s.style = style;
|
style = style.fg(Color::White).add_modifier(Modifier::BOLD);
|
||||||
ListItem::new(vec![Spans::from(s)])
|
} else {
|
||||||
}).collect();
|
style = style.fg(Color::White);
|
||||||
|
}
|
||||||
|
let mut s = Span::raw(task.title.as_str());
|
||||||
|
s.style = style;
|
||||||
|
ListItem::new(vec![Spans::from(s)])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let mut style = Style::default();
|
let mut style = Style::default();
|
||||||
if i == state.selected_column { style = style.fg(Color::Green); };
|
if i == state.project.selected_column_idx {
|
||||||
let mut s = Span::raw(format!("{:?}", status));
|
style = style.fg(Color::Green);
|
||||||
|
};
|
||||||
|
let mut s = Span::raw(format!("{:?}", column.name));
|
||||||
s.style = Style::default()
|
s.style = Style::default()
|
||||||
.add_modifier(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED)
|
.add_modifier(Modifier::BOLD | Modifier::ITALIC | Modifier::UNDERLINED)
|
||||||
.fg(Color::White);
|
.fg(Color::White);
|
||||||
let block = Block::default()
|
let block = Block::default().style(style).title(s).borders(Borders::ALL);
|
||||||
.style(style)
|
|
||||||
.title(s)
|
|
||||||
.borders(Borders::ALL);
|
|
||||||
let list = List::new(items).block(block);
|
let list = List::new(items).block(block);
|
||||||
f.render_widget(list, columns[i])
|
f.render_widget(list, columns[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_task_info<B: Backend>(f: &mut Frame<B>, area: &Rect, state: &AppState) {
|
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").borders(Borders::ALL);
|
||||||
.title("TASK INFO")
|
if let Some(task) = state.project.get_selected_column().get_selected_task() {
|
||||||
.borders(Borders::ALL);
|
let p = Paragraph::new(task.description.as_str())
|
||||||
if let Some(task) = state.get_selected_task() {
|
.block(block)
|
||||||
let p = Paragraph::new(task.description.as_str()).block(block).wrap(Wrap { trim: true });
|
.wrap(Wrap { trim: true });
|
||||||
f.render_widget(p, *area);
|
f.render_widget(p, *area);
|
||||||
} else {
|
} else {
|
||||||
let p = Paragraph::new("No tasks for this column").block(block);
|
let p = Paragraph::new("No tasks for this column").block(block);
|
||||||
f.render_widget(p, *area);
|
f.render_widget(p, *area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn centered_rect_for_popup(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||||
|
let popup_layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Percentage((100 - percent_y) / 2),
|
||||||
|
Constraint::Percentage(percent_y),
|
||||||
|
Constraint::Percentage((100 - percent_y) / 2),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(r);
|
||||||
|
|
||||||
|
Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Percentage((100 - percent_x) / 2),
|
||||||
|
Constraint::Percentage(percent_x),
|
||||||
|
Constraint::Percentage((100 - percent_x) / 2),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(popup_layout[1])[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw_new_task_popup<B: Backend>(f: &mut Frame<B>, state: &mut AppState) {
|
||||||
|
let area = centered_rect_for_popup(45, 60, f.size());
|
||||||
|
f.render_widget(Clear, area);
|
||||||
|
match &state.popup_text {
|
||||||
|
None => {}
|
||||||
|
Some(s) => {
|
||||||
|
let block = Block::default()
|
||||||
|
.title("Add Task")
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
let block_inner = block.inner(area);
|
||||||
|
let main = Paragraph::new(s.as_ref()).block(block);
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Max(100),
|
||||||
|
Constraint::Length(2),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(block_inner);
|
||||||
|
let b1 = Block::default().title("Title").borders(Borders::ALL);
|
||||||
|
let title = Paragraph::new("Hello I am text")
|
||||||
|
// .style(Style::default().fg(Color::Yellow))
|
||||||
|
.block(b1);
|
||||||
|
let b2 = Block::default().title("Description").borders(Borders::ALL);
|
||||||
|
let description = Paragraph::new("Fill this out")
|
||||||
|
// .style(Style::default().fg(Color::Yellow))
|
||||||
|
.block(b2);
|
||||||
|
let b3 = Block::default().title("Keys").borders(Borders::TOP);
|
||||||
|
let footer = Paragraph::new("p : Cancel").block(b3);
|
||||||
|
f.render_widget(main, area);
|
||||||
|
f.render_widget(title, layout[0]);
|
||||||
|
f.render_widget(description, layout[1]);
|
||||||
|
f.render_widget(footer, layout[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn draw<B: Backend>(f: &mut Frame<B>, state: &mut AppState) {
|
pub fn draw<B: Backend>(f: &mut Frame<B>, state: &mut AppState) {
|
||||||
let main_layout = Layout::default()
|
let main_layout = Layout::default()
|
||||||
@ -61,27 +135,29 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, state: &mut AppState) {
|
|||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
Constraint::Percentage(10),
|
Constraint::Percentage(10),
|
||||||
Constraint::Percentage(60),
|
Constraint::Percentage(65),
|
||||||
Constraint::Percentage(20),
|
Constraint::Percentage(20),
|
||||||
Constraint::Percentage(10),
|
Constraint::Length(3),
|
||||||
].as_ref()
|
]
|
||||||
).split(f.size());
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(f.size());
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default().title("KANBAN BOARD").borders(Borders::ALL);
|
||||||
.title("KANBAN BOARD")
|
|
||||||
.borders(Borders::ALL);
|
|
||||||
f.render_widget(block, main_layout[0]);
|
f.render_widget(block, main_layout[0]);
|
||||||
|
|
||||||
draw_tasks(f, &main_layout[1], &state);
|
draw_tasks(f, &main_layout[1], &state);
|
||||||
|
|
||||||
draw_task_info(f, &main_layout[2], &state);
|
draw_task_info(f, &main_layout[2], &state);
|
||||||
|
|
||||||
let block = Block::default()
|
let block = Block::default().title("KEYBINDINGS").borders(Borders::TOP);
|
||||||
.title("KEYBINDINGS")
|
|
||||||
.borders(Borders::ALL);
|
|
||||||
|
|
||||||
let foot_txt =
|
let foot_txt =
|
||||||
Paragraph::new("q : Quit | ⏪🔽🔼⏩ or hjkl : Navigation | < > or H L : Shift task left/right | = - or J K : Shift task up/down")
|
Paragraph::new("q : Quit | ⏪🔽🔼⏩ or hjkl : Navigation | < > or H L : Shift task left/right | = - or J K : Shift task up/down")
|
||||||
.block(block);
|
.block(block);
|
||||||
f.render_widget(foot_txt, main_layout[3]);
|
f.render_widget(foot_txt, main_layout[3]);
|
||||||
}
|
|
||||||
|
if state.popup_text.is_some() {
|
||||||
|
draw_new_task_popup(f, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user