kanban-tui/src/app.rs
2023-06-10 11:12:17 +07:00

262 lines
6.9 KiB
Rust

// use indexmap::IndexMap;
// use int_enum::IntEnum;
use serde::{Deserialize, Serialize};
use std::cmp::min;
use std::fs::File;
use std::io::Read;
use tui_textarea::TextArea;
#[cfg(test)]
mod tests;
#[derive(Debug, Serialize, Deserialize)]
pub struct Column {
pub name: String,
pub selected_task_idx: usize,
pub tasks: Vec<Task>,
}
// #[derive(Deserialize, Serialize, Debug, Clone, Copy)]
#[derive(Default, Deserialize, Serialize, Debug)]
pub struct Task {
pub title: String,
pub description: String,
}
/// Type used mainly for serialization at this time
#[derive(Deserialize, Serialize, Debug)]
pub struct Project {
pub name: String,
pub filepath: String,
pub selected_column_idx: usize,
pub columns: Vec<Column>,
}
#[derive(Debug, thiserror::Error)]
pub enum KanbanError {
#[error("There is something wrong with the json schema, it doesn't match Project struct")]
BadJson,
#[error("IO - {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug)]
pub enum TaskEditFocus {
Title,
Description,
ConfirmBtn,
CancelBtn,
}
pub struct TaskState<'a> {
pub title: TextArea<'a>,
pub description: TextArea<'a>,
pub focus: TaskEditFocus,
pub is_edit: bool,
}
impl Default for TaskState<'_> {
fn default() -> Self {
TaskState {
title: TextArea::default(),
description: TextArea::default(),
focus: TaskEditFocus::Title,
is_edit: false,
}
}
}
pub struct State<'a> {
pub project: Project,
pub quit: bool,
pub columns: Vec<Column>,
pub task_edit_state: Option<TaskState<'a>>,
}
impl State<'_> {
#[must_use]
pub fn new(project: Project) -> Self {
State {
quit: false,
task_edit_state: None,
project,
columns: vec![],
}
}
}
impl<'a> Column {
#[must_use]
pub fn new(name: &str) -> Self {
Column {
name: name.to_owned(),
tasks: vec![],
selected_task_idx: 0,
}
}
pub fn add_task(&mut self, title: String, description: String) {
let task = Task { title, description };
self.tasks.push(task);
self.select_next_task();
}
pub fn remove_task(&mut self) {
self.tasks.remove(self.selected_task_idx);
self.select_next_task();
}
#[must_use]
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().saturating_sub(1));
}
pub fn select_first_task(&mut self) {
self.selected_task_idx = 0;
}
pub fn select_last_task(&mut self) {
self.selected_task_idx = self.tasks.len() - 1;
}
#[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,
})
}
}
impl Project {
#[must_use]
pub fn new(name: &str, filepath: String) -> Self {
Project {
name: name.to_owned(),
filepath,
columns: vec![
Column::new("Todo"),
Column::new("InProgress"),
Column::new("Done"),
Column::new("Ideas"),
],
selected_column_idx: 0,
}
}
fn load_from_json(json: &str) -> Result<Self, KanbanError> {
serde_json::from_str(json).map_err(|_| KanbanError::BadJson)
}
/// # Errors
///
/// Will return `Err` if `file` contains json that doesn't match State schema
pub fn load(path: String, mut file: &File) -> Result<Self, KanbanError> {
let mut json = String::new();
file.read_to_string(&mut json)?;
if json.trim().is_empty() {
Ok(Project::new("", path))
} else {
Self::load_from_json(&json)
}
}
/// # Panics
///
/// Will panic if there's an error serializing the Json or there's an issue
/// writing the file
pub fn save(&self) {
let json = serde_json::to_string_pretty(&self).unwrap();
std::fs::write(&self.filepath, json).unwrap();
}
#[must_use]
pub fn get_selected_column(&self) -> &Column {
&self.columns[self.selected_column_idx]
}
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.is_empty() {
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 fn move_task_previous_column(&mut self) {
self.move_task_to_column(false);
}
pub fn move_task_next_column(&mut self) {
self.move_task_to_column(true);
}
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.selected_task_idx -= 1;
self.save();
}
}
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.selected_task_idx += 1;
self.save();
}
}
}