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

View File

@ -1,6 +1,6 @@
use crate::app::{State, TaskEditFocus, TaskState};
use crossterm::event;
use crossterm::event::{Event, KeyCode};
use crate::app::{TaskState, State, TaskEditFocus};
/// # Errors
///
@ -14,81 +14,76 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> {
// TODO: Extract this code to a separate function to avoid nesting
match task.focus {
// TODO: Handle wrapping around the enum rather than doing it manually
TaskEditFocus::Title => {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::Description,
KeyCode::BackTab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::Enter => (),
_ => { task.title.input(key); }
TaskEditFocus::Title => match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::Description,
KeyCode::BackTab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::Enter => (),
_ => {
task.title.input(key);
}
}
TaskEditFocus::Description => {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::ConfirmBtn,
KeyCode::BackTab => task.focus = TaskEditFocus::Title,
_ => { task.description.input(key); }
},
TaskEditFocus::Description => match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::ConfirmBtn,
KeyCode::BackTab => task.focus = TaskEditFocus::Title,
_ => {
task.description.input(key);
}
}
TaskEditFocus::ConfirmBtn => {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::BackTab => task.focus = TaskEditFocus::Description,
KeyCode::Enter => {
let title = task.title.clone().into_lines().join("\n");
let description = task.description.clone().into_lines().join("\n");
if task.is_edit {
if let Some(selected_task) = column.get_selected_task_mut() {
selected_task.title = title;
selected_task.description = description;
}
} else {
column.add_task(title, description);
},
TaskEditFocus::ConfirmBtn => match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::CancelBtn,
KeyCode::BackTab => task.focus = TaskEditFocus::Description,
KeyCode::Enter => {
let title = task.title.clone().into_lines().join("\n");
let description = task.description.clone().into_lines().join("\n");
if task.is_edit {
if let Some(selected_task) = column.get_selected_task_mut() {
selected_task.title = title;
selected_task.description = description;
}
state.task_edit_state = None;
project.save();
} else {
column.add_task(title, description);
}
_ => (),
state.task_edit_state = None;
project.save();
}
}
TaskEditFocus::CancelBtn => {
match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::Title,
KeyCode::BackTab => task.focus = TaskEditFocus::ConfirmBtn,
KeyCode::Enter => {
state.task_edit_state = None;
}
_ => (),
_ => (),
},
TaskEditFocus::CancelBtn => match key.code {
KeyCode::Tab => task.focus = TaskEditFocus::Title,
KeyCode::BackTab => task.focus = TaskEditFocus::ConfirmBtn,
KeyCode::Enter => {
state.task_edit_state = None;
}
}
_ => (),
},
};
}
None => {
match key.code {
KeyCode::Char('q') => state.quit = true,
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();
},
_ => {}
None => match key.code {
KeyCode::Char('q') => state.quit = true,
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();
}
_ => {}
},
}
}
Ok(())

View File

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

View File

@ -1,23 +1,18 @@
#![deny(rust_2018_idioms)]
use kanban_tui::{Project, State};
use clap::{Parser, ValueHint::FilePath};
use crossterm::{
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 std::{
error::Error,
fs::{File, OpenOptions},
io::{self, Write},
path::PathBuf,
fs::{File, OpenOptions},
error::Error
};
use tui::backend::CrosstermBackend;
use tui::Terminal;
use clap::{Parser, ValueHint::FilePath};
const DEFAULT_DATABASE_NAME: &str = "kanban.json";
@ -27,7 +22,7 @@ const DEFAULT_DATABASE_NAME: &str = "kanban.json";
pub struct CliArgs {
#[arg(value_name="DATABASE", value_hint=FilePath, index=1)]
/// Path to the
pub filepath: Option<PathBuf>
pub filepath: Option<PathBuf>,
}
// TODO: This should just return a struct beacuse we should add a
@ -42,54 +37,55 @@ fn prompt_project_init(default_name: &str) -> (String, io::Result<File>) {
let result = io::stdin().read_line(&mut input);
let input = input.trim();
let filename =
match result {
Ok(b) if b == 0 => std::process::exit(0),
Ok(b) if b > 0 && !input.is_empty() => input,
_ => default_name
};
let filename = match result {
Ok(b) if b == 0 => std::process::exit(0),
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
// to change the default column names
(filename.to_string(),
OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(filename))
(
filename.to_string(),
OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(filename),
)
}
fn main() -> anyhow::Result<(), Box<dyn Error>> {
let (filepath, file) =
match CliArgs::parse() {
CliArgs { filepath: Some(filepath) } => {
let fpath = filepath.into_os_string().into_string().unwrap();
let file = OpenOptions::new()
.write(true)
.read(true)
.open(&fpath);
#[async_std::main]
async fn main() -> anyhow::Result<(), Box<dyn Error>> {
let (filepath, file) = match CliArgs::parse() {
CliArgs {
filepath: Some(filepath),
} => {
let fpath = filepath.into_os_string().into_string().unwrap();
let file = OpenOptions::new().write(true).read(true).open(&fpath);
if let Ok(f) = file {
(fpath, f)
} else {
let (fp, fname) = prompt_project_init(&fpath);
(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())
}
if let Ok(f) = file {
(fpath, f)
} else {
let (fp, fname) = prompt_project_init(&fpath);
(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 mut state = State::new(Project::load(filepath, &file)?);
enable_raw_mode()?;

View File

@ -19,7 +19,8 @@ fn draw_tasks<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &State<'_>) {
.split(area);
for (i, column) in state.project.columns.iter().enumerate() {
let items: Vec<ListItem<'_>> = column.tasks
let items: Vec<ListItem<'_>> = column
.tasks
.iter()
.enumerate()
.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::Min(10),
Constraint::Min(10)
Constraint::Min(10),
]
.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);
if let TaskEditFocus::Title = task.focus {
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 {
task.title.set_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);
if let TaskEditFocus::Description = task.focus {
task.description.set_style(Style::default().fg(Color::Yellow));
task.description.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
task.description
.set_style(Style::default().fg(Color::Yellow));
task.description
.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
} else {
task.description.set_style(Style::default());
task.description.set_cursor_style(Style::default());