Compare commits

..

No commits in common. "a8e3a448168089d8a94e7cb80ec858a9919420c0" and "e3ce612ca375732d1af7d38f67eaa363a2897e9c" have entirely different histories.

7 changed files with 146 additions and 1306 deletions

1157
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,4 @@ serde_json = "1.0.89"
int-enum = "0.5.0" int-enum = "0.5.0"
thiserror = "1" thiserror = "1"
anyhow = "1" anyhow = "1"
sqlx = { version = "0.6", features = [ "runtime-async-std-native-tls", "sqlite" ] }
async-std = { version = "1", features = [ "attributes" ] }
clap = { version = "4.3.2" , features = [ "derive" ] } clap = { version = "4.3.2" , features = [ "derive" ] }

View File

@ -2,7 +2,7 @@
// use int_enum::IntEnum; // use int_enum::IntEnum;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::cmp::min; use std::cmp::min;
use std::fs::File; use std::fs::{File};
use std::io::Read; use std::io::Read;
use tui_textarea::TextArea; use tui_textarea::TextArea;
@ -29,7 +29,7 @@ pub struct Project {
pub name: String, pub name: String,
pub filepath: String, pub filepath: String,
pub selected_column_idx: usize, pub selected_column_idx: usize,
pub columns: Vec<Column>, pub columns: Vec<Column>
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -52,7 +52,7 @@ pub struct TaskState<'a> {
pub title: TextArea<'a>, pub title: TextArea<'a>,
pub description: TextArea<'a>, pub description: TextArea<'a>,
pub focus: TaskEditFocus, pub focus: TaskEditFocus,
pub is_edit: bool, pub is_edit: bool
} }
impl Default for TaskState<'_> { impl Default for TaskState<'_> {
@ -61,7 +61,7 @@ impl Default for TaskState<'_> {
title: TextArea::default(), title: TextArea::default(),
description: TextArea::default(), description: TextArea::default(),
focus: TaskEditFocus::Title, focus: TaskEditFocus::Title,
is_edit: false, is_edit: false
} }
} }
} }
@ -135,11 +135,13 @@ impl<'a> Column {
#[must_use] #[must_use]
pub fn get_task_state_from_curr_selected_task(&self) -> Option<TaskState<'a>> { pub fn get_task_state_from_curr_selected_task(&self) -> Option<TaskState<'a>> {
self.get_selected_task().map(|t| TaskState { self.get_selected_task().map(|t| {
title: TextArea::from(t.title.lines()), TaskState {
description: TextArea::from(t.description.lines()), title: TextArea::from(t.title.lines()),
focus: TaskEditFocus::Title, description: TextArea::from(t.description.lines()),
is_edit: true, focus: TaskEditFocus::Title,
is_edit: true
}
}) })
} }
} }
@ -201,7 +203,10 @@ impl Project {
} }
pub fn select_next_column(&mut self) -> &Column { pub fn select_next_column(&mut self) -> &Column {
self.selected_column_idx = min(self.selected_column_idx + 1, self.columns.len() - 1); self.selected_column_idx = min(
self.selected_column_idx + 1,
self.columns.len() - 1,
);
&self.columns[self.selected_column_idx] &self.columns[self.selected_column_idx]
} }
@ -240,9 +245,7 @@ impl Project {
pub fn move_task_up(&mut self) { pub fn move_task_up(&mut self) {
let column = self.get_selected_column_mut(); let column = self.get_selected_column_mut();
if column.selected_task_idx > 0 { if column.selected_task_idx > 0 {
column column.tasks.swap(column.selected_task_idx, column.selected_task_idx - 1);
.tasks
.swap(column.selected_task_idx, column.selected_task_idx - 1);
column.selected_task_idx -= 1; column.selected_task_idx -= 1;
self.save(); self.save();
} }
@ -251,9 +254,7 @@ impl Project {
pub fn move_task_down(&mut self) { pub fn move_task_down(&mut self) {
let column = self.get_selected_column_mut(); let column = self.get_selected_column_mut();
if column.selected_task_idx < column.tasks.len() - 1 { if column.selected_task_idx < column.tasks.len() - 1 {
column column.tasks.swap(column.selected_task_idx, column.selected_task_idx + 1);
.tasks
.swap(column.selected_task_idx, column.selected_task_idx + 1);
column.selected_task_idx += 1; column.selected_task_idx += 1;
self.save(); self.save();
} }

View File

@ -1,6 +1,6 @@
use crate::app::{State, TaskEditFocus, TaskState};
use crossterm::event; use crossterm::event;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use crate::app::{TaskState, State, TaskEditFocus};
/// # Errors /// # Errors
/// ///
@ -14,76 +14,81 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> {
// TODO: Extract this code to a separate function to avoid nesting // TODO: Extract this code to a separate function to avoid nesting
match task.focus { match task.focus {
// TODO: Handle wrapping around the enum rather than doing it manually // TODO: Handle wrapping around the enum rather than doing it manually
TaskEditFocus::Title => match key.code { TaskEditFocus::Title => {
KeyCode::Tab => task.focus = TaskEditFocus::Description, match key.code {
KeyCode::BackTab => task.focus = TaskEditFocus::CancelBtn, KeyCode::Tab => task.focus = TaskEditFocus::Description,
KeyCode::Enter => (), KeyCode::BackTab => task.focus = TaskEditFocus::CancelBtn,
_ => { KeyCode::Enter => (),
task.title.input(key); _ => { task.title.input(key); }
} }
}, }
TaskEditFocus::Description => match key.code { TaskEditFocus::Description => {
KeyCode::Tab => task.focus = TaskEditFocus::ConfirmBtn, match key.code {
KeyCode::BackTab => task.focus = TaskEditFocus::Title, KeyCode::Tab => task.focus = TaskEditFocus::ConfirmBtn,
_ => { KeyCode::BackTab => task.focus = TaskEditFocus::Title,
task.description.input(key); _ => { task.description.input(key); }
} }
}, }
TaskEditFocus::ConfirmBtn => match key.code { TaskEditFocus::ConfirmBtn => {
KeyCode::Tab => task.focus = TaskEditFocus::CancelBtn, match key.code {
KeyCode::BackTab => task.focus = TaskEditFocus::Description, KeyCode::Tab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::Enter => { KeyCode::BackTab => task.focus = TaskEditFocus::Description,
let title = task.title.clone().into_lines().join("\n"); KeyCode::Enter => {
let description = task.description.clone().into_lines().join("\n"); let title = task.title.clone().into_lines().join("\n");
if task.is_edit { let description = task.description.clone().into_lines().join("\n");
if let Some(selected_task) = column.get_selected_task_mut() { if task.is_edit {
selected_task.title = title; if let Some(selected_task) = column.get_selected_task_mut() {
selected_task.description = description; selected_task.title = title;
selected_task.description = description;
}
} else {
column.add_task(title, description);
} }
} else { state.task_edit_state = None;
column.add_task(title, description); project.save();
} }
state.task_edit_state = None; _ => (),
project.save();
} }
_ => (), }
}, TaskEditFocus::CancelBtn => {
TaskEditFocus::CancelBtn => match key.code { match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::Title, KeyCode::Tab => task.focus = TaskEditFocus::Title,
KeyCode::BackTab => task.focus = TaskEditFocus::ConfirmBtn, KeyCode::BackTab => task.focus = TaskEditFocus::ConfirmBtn,
KeyCode::Enter => { KeyCode::Enter => {
state.task_edit_state = None; state.task_edit_state = None;
}
_ => (),
} }
_ => (), }
},
}; };
} }
None => match key.code { None => {
KeyCode::Char('q') => state.quit = true, match key.code {
KeyCode::Char('h') | KeyCode::Left => { KeyCode::Char('q') => state.quit = true,
project.select_previous_column(); KeyCode::Char('h') |
KeyCode::Left => { project.select_previous_column(); },
KeyCode::Char('j') |
KeyCode::Down => column.select_next_task(),
KeyCode::Char('k') |
KeyCode::Up => column.select_previous_task(),
KeyCode::Char('l') |
KeyCode::Right => { project.select_next_column(); },
KeyCode::Char('g') => column.select_first_task(),
KeyCode::Char('G') => column.select_last_task(),
KeyCode::Char('H') => project.move_task_previous_column(),
KeyCode::Char('L') => project.move_task_next_column(),
KeyCode::Char('J') => project.move_task_down(),
KeyCode::Char('K') => project.move_task_up(),
KeyCode::Char('n') => state.task_edit_state = Some(TaskState::default()),
KeyCode::Char('e') =>
state.task_edit_state = column.get_task_state_from_curr_selected_task(),
KeyCode::Char('D') => {
column.remove_task();
project.save();
},
_ => {}
} }
KeyCode::Char('j') | KeyCode::Down => column.select_next_task(), }
KeyCode::Char('k') | KeyCode::Up => column.select_previous_task(),
KeyCode::Char('l') | KeyCode::Right => {
project.select_next_column();
}
KeyCode::Char('g') => column.select_first_task(),
KeyCode::Char('G') => column.select_last_task(),
KeyCode::Char('H') => project.move_task_previous_column(),
KeyCode::Char('L') => project.move_task_next_column(),
KeyCode::Char('J') => project.move_task_down(),
KeyCode::Char('K') => project.move_task_up(),
KeyCode::Char('n') => state.task_edit_state = Some(TaskState::default()),
KeyCode::Char('e') => {
state.task_edit_state = column.get_task_state_from_curr_selected_task()
}
KeyCode::Char('D') => {
column.remove_task();
project.save();
}
_ => {}
},
} }
} }
Ok(()) Ok(())

View File

@ -1,8 +1,8 @@
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
mod app; mod app;
mod input;
mod ui; mod ui;
mod input;
pub use app::*; pub use app::*;
pub use input::handle;
pub use ui::draw; pub use ui::draw;
pub use input::handle;

View File

@ -1,19 +1,23 @@
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
use clap::{Parser, ValueHint::FilePath}; use kanban_tui::{Project, State};
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture}, event::{DisableMouseCapture, EnableMouseCapture},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{
disable_raw_mode,
enable_raw_mode,
EnterAlternateScreen,
LeaveAlternateScreen
},
}; };
use kanban_tui::{Project, State};
use sqlx::sqlite::SqlitePool;
use std::{ use std::{
error::Error,
fs::{File, OpenOptions},
io::{self, Write}, io::{self, Write},
path::PathBuf, path::PathBuf,
fs::{File, OpenOptions},
error::Error
}; };
use tui::backend::CrosstermBackend; use tui::backend::CrosstermBackend;
use tui::Terminal; use tui::Terminal;
use clap::{Parser, ValueHint::FilePath};
const DEFAULT_DATABASE_NAME: &str = "kanban.json"; const DEFAULT_DATABASE_NAME: &str = "kanban.json";
@ -23,7 +27,7 @@ const DEFAULT_DATABASE_NAME: &str = "kanban.json";
pub struct CliArgs { pub struct CliArgs {
#[arg(value_name="DATABASE", value_hint=FilePath, index=1)] #[arg(value_name="DATABASE", value_hint=FilePath, index=1)]
/// Path to the /// Path to the
pub filepath: Option<PathBuf>, pub filepath: Option<PathBuf>
} }
// TODO: This should just return a struct beacuse we should add a // TODO: This should just return a struct beacuse we should add a
@ -38,69 +42,54 @@ fn prompt_project_init(default_name: &str) -> (String, io::Result<File>) {
let result = io::stdin().read_line(&mut input); let result = io::stdin().read_line(&mut input);
let input = input.trim(); let input = input.trim();
let filename = match result { let filename =
Ok(b) if b == 0 => std::process::exit(0), match result {
Ok(b) if b > 0 && !input.is_empty() => input, Ok(b) if b == 0 => std::process::exit(0),
_ => default_name, Ok(b) if b > 0 && !input.is_empty() => input,
}; _ => default_name
};
// TODO: This might be a good time to prompt the user if they want // TODO: This might be a good time to prompt the user if they want
// to change the default column names // to change the default column names
( (filename.to_string(),
filename.to_string(), OpenOptions::new()
OpenOptions::new() .write(true)
.write(true) .read(true)
.read(true) .create(true)
.create(true) .open(filename))
.open(filename),
)
} }
#[async_std::main] fn main() -> anyhow::Result<(), Box<dyn Error>> {
async fn main() -> anyhow::Result<(), Box<dyn Error>> { let (filepath, file) =
let (filepath, file) = match CliArgs::parse() { match CliArgs::parse() {
CliArgs { CliArgs { filepath: Some(filepath) } => {
filepath: Some(filepath), let fpath = filepath.into_os_string().into_string().unwrap();
} => { let file = OpenOptions::new()
let fpath = filepath.into_os_string().into_string().unwrap(); .write(true)
let file = OpenOptions::new().write(true).read(true).open(&fpath); .read(true)
.open(&fpath);
if let Ok(f) = file { if let Ok(f) = file {
(fpath, f) (fpath, f)
} else { } else {
let (fp, fname) = prompt_project_init(&fpath); let (fp, fname) = prompt_project_init(&fpath);
(fp, fname.unwrap()) (fp, fname.unwrap())
}
},
CliArgs { filepath: None } => {
let file = OpenOptions::new()
.write(true)
.read(true)
.open(DEFAULT_DATABASE_NAME);
if let Ok(f) = file {
(DEFAULT_DATABASE_NAME.to_string(), f)
} else {
let (fp, fname) = prompt_project_init(DEFAULT_DATABASE_NAME);
(fp, fname.unwrap())
}
} }
} };
CliArgs { filepath: None } => {
let file = OpenOptions::new()
.write(true)
.read(true)
.open(DEFAULT_DATABASE_NAME);
if let Ok(f) = file {
(DEFAULT_DATABASE_NAME.to_string(), f)
} else {
let (fp, fname) = prompt_project_init(DEFAULT_DATABASE_NAME);
(fp, fname.unwrap())
}
}
};
let pool = SqlitePool::connect("sqlite:db.sqlite").await?;
let stuff = sqlx::query!(
r#"
select * from kanban
"#
)
.fetch_all(&pool)
.await?;
for item in stuff {
println!("{} - {} - {}", item.id, item.name, item.description);
}
let mut state = State::new(Project::load(filepath, &file)?); let mut state = State::new(Project::load(filepath, &file)?);
enable_raw_mode()?; enable_raw_mode()?;

View File

@ -19,8 +19,7 @@ fn draw_tasks<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
.split(area); .split(area);
for (i, column) in state.project.columns.iter().enumerate() { for (i, column) in state.project.columns.iter().enumerate() {
let items: Vec<ListItem<'_>> = column let items: Vec<ListItem<'_>> = column.tasks
.tasks
.iter() .iter()
.enumerate() .enumerate()
.map(|(j, task)| { .map(|(j, task)| {
@ -124,7 +123,7 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
[ [
Constraint::Percentage(80), Constraint::Percentage(80),
Constraint::Min(10), Constraint::Min(10),
Constraint::Min(10), Constraint::Min(10)
] ]
.as_ref(), .as_ref(),
) )
@ -168,8 +167,7 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
task.title.set_block(b1); task.title.set_block(b1);
if let TaskEditFocus::Title = task.focus { if let TaskEditFocus::Title = task.focus {
task.title.set_style(Style::default().fg(Color::Yellow)); task.title.set_style(Style::default().fg(Color::Yellow));
task.title task.title.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
} else { } else {
task.title.set_style(Style::default()); task.title.set_style(Style::default());
task.title.set_cursor_style(Style::default()); task.title.set_cursor_style(Style::default());
@ -178,10 +176,8 @@ pub fn draw_task_popup<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>,
task.description.set_block(b2); task.description.set_block(b2);
if let TaskEditFocus::Description = task.focus { if let TaskEditFocus::Description = task.focus {
task.description task.description.set_style(Style::default().fg(Color::Yellow));
.set_style(Style::default().fg(Color::Yellow)); task.description.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
task.description
.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
} else { } else {
task.description.set_style(Style::default()); task.description.set_style(Style::default());
task.description.set_cursor_style(Style::default()); task.description.set_cursor_style(Style::default());