This commit is contained in:
Joseph Ferano 2023-06-10 11:12:17 +07:00
parent e3ce612ca3
commit 5d3e69d679
5 changed files with 136 additions and 142 deletions

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,13 +135,11 @@ 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| { self.get_selected_task().map(|t| TaskState {
TaskState {
title: TextArea::from(t.title.lines()), title: TextArea::from(t.title.lines()),
description: TextArea::from(t.description.lines()), description: TextArea::from(t.description.lines()),
focus: TaskEditFocus::Title, focus: TaskEditFocus::Title,
is_edit: true is_edit: true,
}
}) })
} }
} }
@ -203,10 +201,7 @@ 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 = min(self.selected_column_idx + 1, self.columns.len() - 1);
self.selected_column_idx + 1,
self.columns.len() - 1,
);
&self.columns[self.selected_column_idx] &self.columns[self.selected_column_idx]
} }
@ -245,7 +240,9 @@ 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.tasks.swap(column.selected_task_idx, column.selected_task_idx - 1); column
.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();
} }
@ -254,7 +251,9 @@ 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.tasks.swap(column.selected_task_idx, column.selected_task_idx + 1); column
.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,23 +14,22 @@ 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 => { TaskEditFocus::Title => match key.code {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::Description, KeyCode::Tab => task.focus = TaskEditFocus::Description,
KeyCode::BackTab => task.focus = TaskEditFocus::CancelBtn, KeyCode::BackTab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::Enter => (), KeyCode::Enter => (),
_ => { task.title.input(key); } _ => {
task.title.input(key);
} }
} },
TaskEditFocus::Description => { TaskEditFocus::Description => match key.code {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::ConfirmBtn, KeyCode::Tab => task.focus = TaskEditFocus::ConfirmBtn,
KeyCode::BackTab => task.focus = TaskEditFocus::Title, KeyCode::BackTab => task.focus = TaskEditFocus::Title,
_ => { task.description.input(key); } _ => {
task.description.input(key);
} }
} },
TaskEditFocus::ConfirmBtn => { TaskEditFocus::ConfirmBtn => match key.code {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::CancelBtn, KeyCode::Tab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::BackTab => task.focus = TaskEditFocus::Description, KeyCode::BackTab => task.focus = TaskEditFocus::Description,
KeyCode::Enter => { KeyCode::Enter => {
@ -48,31 +47,27 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> {
project.save(); project.save();
} }
_ => (), _ => (),
} },
} TaskEditFocus::CancelBtn => match key.code {
TaskEditFocus::CancelBtn => {
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 => { None => 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 => {
KeyCode::Left => { project.select_previous_column(); }, project.select_previous_column();
KeyCode::Char('j') | }
KeyCode::Down => column.select_next_task(), KeyCode::Char('j') | KeyCode::Down => column.select_next_task(),
KeyCode::Char('k') | KeyCode::Char('k') | KeyCode::Up => column.select_previous_task(),
KeyCode::Up => column.select_previous_task(), KeyCode::Char('l') | KeyCode::Right => {
KeyCode::Char('l') | project.select_next_column();
KeyCode::Right => { project.select_next_column(); }, }
KeyCode::Char('g') => column.select_first_task(), KeyCode::Char('g') => column.select_first_task(),
KeyCode::Char('G') => column.select_last_task(), KeyCode::Char('G') => column.select_last_task(),
KeyCode::Char('H') => project.move_task_previous_column(), KeyCode::Char('H') => project.move_task_previous_column(),
@ -80,15 +75,15 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> {
KeyCode::Char('J') => project.move_task_down(), KeyCode::Char('J') => project.move_task_down(),
KeyCode::Char('K') => project.move_task_up(), KeyCode::Char('K') => project.move_task_up(),
KeyCode::Char('n') => state.task_edit_state = Some(TaskState::default()), KeyCode::Char('n') => state.task_edit_state = Some(TaskState::default()),
KeyCode::Char('e') => KeyCode::Char('e') => {
state.task_edit_state = column.get_task_state_from_curr_selected_task(), state.task_edit_state = column.get_task_state_from_curr_selected_task()
}
KeyCode::Char('D') => { KeyCode::Char('D') => {
column.remove_task(); column.remove_task();
project.save(); project.save();
}, }
_ => {} _ => {}
} },
}
} }
} }
Ok(()) Ok(())

View File

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

View File

@ -1,23 +1,18 @@
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
use kanban_tui::{Project, State}; use clap::{Parser, ValueHint::FilePath};
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture}, event::{DisableMouseCapture, EnableMouseCapture},
terminal::{ terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
disable_raw_mode,
enable_raw_mode,
EnterAlternateScreen,
LeaveAlternateScreen
},
}; };
use kanban_tui::{Project, State};
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";
@ -27,7 +22,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
@ -42,33 +37,33 @@ 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 = let filename = match result {
match result {
Ok(b) if b == 0 => std::process::exit(0), Ok(b) if b == 0 => std::process::exit(0),
Ok(b) if b > 0 && !input.is_empty() => input, Ok(b) if b > 0 && !input.is_empty() => input,
_ => default_name _ => 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),
)
} }
fn main() -> anyhow::Result<(), Box<dyn Error>> { #[async_std::main]
let (filepath, file) = async fn main() -> anyhow::Result<(), Box<dyn Error>> {
match CliArgs::parse() { let (filepath, file) = match CliArgs::parse() {
CliArgs { filepath: Some(filepath) } => { CliArgs {
filepath: Some(filepath),
} => {
let fpath = filepath.into_os_string().into_string().unwrap(); let fpath = filepath.into_os_string().into_string().unwrap();
let file = OpenOptions::new() let file = OpenOptions::new().write(true).read(true).open(&fpath);
.write(true)
.read(true)
.open(&fpath);
if let Ok(f) = file { if let Ok(f) = file {
(fpath, f) (fpath, f)
@ -76,7 +71,7 @@ fn main() -> anyhow::Result<(), Box<dyn Error>> {
let (fp, fname) = prompt_project_init(&fpath); let (fp, fname) = prompt_project_init(&fpath);
(fp, fname.unwrap()) (fp, fname.unwrap())
} }
}, }
CliArgs { filepath: None } => { CliArgs { filepath: None } => {
let file = OpenOptions::new() let file = OpenOptions::new()
.write(true) .write(true)
@ -90,6 +85,7 @@ fn main() -> anyhow::Result<(), Box<dyn Error>> {
} }
} }
}; };
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,7 +19,8 @@ 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.tasks let items: Vec<ListItem<'_>> = column
.tasks
.iter() .iter()
.enumerate() .enumerate()
.map(|(j, task)| { .map(|(j, task)| {
@ -123,7 +124,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(),
) )
@ -167,7 +168,8 @@ 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.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); task.title
.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());
@ -176,8 +178,10 @@ 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.set_style(Style::default().fg(Color::Yellow)); task.description
task.description.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED)); .set_style(Style::default().fg(Color::Yellow));
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());