Add docs to everything public. Rename some methods.Remove unused deps
This commit is contained in:
parent
9a779b1abb
commit
4beda4fae7
68
Cargo.lock
generated
68
Cargo.lock
generated
@ -301,13 +301,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "kanban_tui"
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kanban-tui"
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -316,9 +310,6 @@ dependencies = [
|
|||||||
"int-enum",
|
"int-enum",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror",
|
|
||||||
"tui-textarea",
|
"tui-textarea",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -492,49 +483,12 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.163"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.163"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.18",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.96"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.15"
|
version = "0.3.15"
|
||||||
@ -599,26 +553,6 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.18",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "kanban-tui"
|
name = "kanban_tui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@ -9,10 +9,7 @@ edition = "2021"
|
|||||||
tui = { package = "ratatui", version = "0.20.1" }
|
tui = { package = "ratatui", version = "0.20.1" }
|
||||||
tui-textarea = { version = "0.2.0", git = "https://github.com/rhysd/tui-textarea.git", features = ["ratatui-crossterm"], default-features=false }
|
tui-textarea = { version = "0.2.0", git = "https://github.com/rhysd/tui-textarea.git", features = ["ratatui-crossterm"], default-features=false }
|
||||||
crossterm = "0.26.1"
|
crossterm = "0.26.1"
|
||||||
serde = { version = "1.0.148" , features = [ "derive" ] }
|
|
||||||
serde_json = "1.0.89"
|
|
||||||
int-enum = "0.5.0"
|
int-enum = "0.5.0"
|
||||||
thiserror = "1"
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
clap = { version = "4.3.2" , features = [ "derive" ] }
|
clap = { version = "4.3.2" , features = [ "derive" ] }
|
||||||
rusqlite = { version = "0.29", features = [ "bundled" ] }
|
rusqlite = { version = "0.29", features = [ "bundled" ] }
|
113
src/app.rs
113
src/app.rs
@ -1,42 +1,65 @@
|
|||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use int_enum::IntEnum;
|
use int_enum::IntEnum;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
/// Represents a kanban column containing the tasks and other metadata.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Column {
|
pub struct Column {
|
||||||
|
/// Id provided by the database
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
/// The name used for the title in the UI
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
/// The currently selected [`Task`], which keeps track of the
|
||||||
|
/// user's position in a column when the go from one to another
|
||||||
pub selected_task_idx: usize,
|
pub selected_task_idx: usize,
|
||||||
|
/// The collection of [`Task`]
|
||||||
pub tasks: Vec<Task>,
|
pub tasks: Vec<Task>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize, Serialize, Debug)]
|
/// Basic TODO task with a title and a description.
|
||||||
|
#[derive(Clone, Default, Debug)]
|
||||||
pub struct Task {
|
pub struct Task {
|
||||||
|
/// Id provided by the database
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
/// Title of the [`Task`]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
/// Description of the [`Task`]
|
||||||
pub description: String,
|
pub description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The number of TaskEditFocus variants, used so we can "wrap around"
|
||||||
|
/// with modulo when cycling through tasks with Tab/Backtab.
|
||||||
pub const EDIT_WINDOW_FOCUS_STATES: i8 = 4;
|
pub const EDIT_WINDOW_FOCUS_STATES: i8 = 4;
|
||||||
|
|
||||||
|
/// Used to track the focus of the form field in the task edit window.
|
||||||
#[repr(i8)]
|
#[repr(i8)]
|
||||||
#[derive(Debug, IntEnum, Copy, Clone)]
|
#[derive(Debug, IntEnum, Copy, Clone)]
|
||||||
pub enum TaskEditFocus {
|
pub enum TaskEditFocus {
|
||||||
|
/// Title text input line
|
||||||
Title = 0,
|
Title = 0,
|
||||||
|
/// Description text input box
|
||||||
Description = 1,
|
Description = 1,
|
||||||
|
/// Confirm changes button
|
||||||
ConfirmBtn = 2,
|
ConfirmBtn = 2,
|
||||||
|
/// Cancel changes button
|
||||||
CancelBtn = 3,
|
CancelBtn = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the transient state of a task while it is being editing
|
||||||
|
/// by the user through the UI.
|
||||||
pub struct TaskState<'a> {
|
pub struct TaskState<'a> {
|
||||||
|
/// The title of the Task
|
||||||
pub title: TextArea<'a>,
|
pub title: TextArea<'a>,
|
||||||
|
/// The description of the Task
|
||||||
pub description: TextArea<'a>,
|
pub description: TextArea<'a>,
|
||||||
|
/// Where the current focus of the task edit form is
|
||||||
pub focus: TaskEditFocus,
|
pub focus: TaskEditFocus,
|
||||||
|
/// Used to decide if the user is editing an existing task or
|
||||||
|
/// creating a new one
|
||||||
pub is_edit: bool,
|
pub is_edit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,12 +74,21 @@ impl Default for TaskState<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Holds the application's state, including all columns and the
|
||||||
|
/// database connection.
|
||||||
pub struct State<'a> {
|
pub struct State<'a> {
|
||||||
|
/// The name of the project, currently derived from the name of
|
||||||
|
/// the current working directory
|
||||||
pub project_name: String,
|
pub project_name: String,
|
||||||
|
/// The index of the currently selected [`Column`]
|
||||||
pub selected_column_idx: usize,
|
pub selected_column_idx: usize,
|
||||||
|
/// A vec of all the [`Column`]s
|
||||||
pub columns: Vec<Column>,
|
pub columns: Vec<Column>,
|
||||||
|
/// The [`db::DBConn`] wrapping a [`rusqlite::Connection`]
|
||||||
pub db_conn: db::DBConn,
|
pub db_conn: db::DBConn,
|
||||||
|
/// Flag to check on each loop whether we should exit the app
|
||||||
pub quit: bool,
|
pub quit: bool,
|
||||||
|
/// If [`Some(TaskState)`] then we are in the task edit form window
|
||||||
pub task_edit_state: Option<TaskState<'a>>,
|
pub task_edit_state: Option<TaskState<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,33 +119,44 @@ impl<'a> State<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the currently selected [`Column`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_selected_column(&self) -> &Column {
|
pub fn get_selected_column(&self) -> &Column {
|
||||||
&self.columns[self.selected_column_idx]
|
&self.columns[self.selected_column_idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the currently selected
|
||||||
|
/// [`Column`].
|
||||||
pub fn get_selected_column_mut(&mut self) -> &mut Column {
|
pub fn get_selected_column_mut(&mut self) -> &mut Column {
|
||||||
&mut self.columns[self.selected_column_idx]
|
&mut self.columns[self.selected_column_idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_previous_column(&mut self) -> Result<(), Error> {
|
/// Selects the [`Column`] on the left. Does nothing if on the
|
||||||
|
/// first column.
|
||||||
|
pub fn select_column_left(&mut self) -> Result<(), Error> {
|
||||||
self.selected_column_idx = self.selected_column_idx.saturating_sub(1);
|
self.selected_column_idx = self.selected_column_idx.saturating_sub(1);
|
||||||
self.db_conn.set_selected_column(self.selected_column_idx)
|
self.db_conn.set_selected_column(self.selected_column_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_next_column(&mut self) -> Result<(), Error> {
|
/// Selects the [`Column`] on the right. Does nothing if on the
|
||||||
|
/// last column.
|
||||||
|
pub fn select_column_right(&mut self) -> Result<(), Error> {
|
||||||
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.db_conn.set_selected_column(self.selected_column_idx)
|
self.db_conn.set_selected_column(self.selected_column_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the currently selected [`Task`].
|
||||||
|
/// Returns `None` if the current [`Column::tasks`] is empty.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_selected_task(&self) -> Option<&Task> {
|
pub fn get_selected_task(&self) -> Option<&Task> {
|
||||||
let column = self.get_selected_column();
|
let column = self.get_selected_column();
|
||||||
column.tasks.get(column.selected_task_idx)
|
column.tasks.get(column.selected_task_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the [`Task`] above the current one.
|
||||||
|
/// Returns `None` if it's the first task on the list
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_previous_task(&self) -> Option<&Task> {
|
pub fn get_task_above(&self) -> Option<&Task> {
|
||||||
let column = self.get_selected_column();
|
let column = self.get_selected_column();
|
||||||
if column.selected_task_idx > 0 {
|
if column.selected_task_idx > 0 {
|
||||||
column.tasks.get(column.selected_task_idx - 1)
|
column.tasks.get(column.selected_task_idx - 1)
|
||||||
@ -122,18 +165,25 @@ impl<'a> State<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the [`Task`] below the current one.
|
||||||
|
/// Returns `None` if it's the last task on the list
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_next_task(&self) -> Option<&Task> {
|
pub fn get_task_below(&self) -> Option<&Task> {
|
||||||
let column = self.get_selected_column();
|
let column = self.get_selected_column();
|
||||||
column.tasks.get(column.selected_task_idx + 1)
|
column.tasks.get(column.selected_task_idx + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the currently selected
|
||||||
|
/// [`Task`]. Returns `None` if the current [`Column::tasks`] is
|
||||||
|
/// empty.
|
||||||
pub fn get_selected_task_mut(&mut self) -> Option<&mut Task> {
|
pub fn get_selected_task_mut(&mut self) -> Option<&mut Task> {
|
||||||
let column = self.get_selected_column_mut();
|
let column = self.get_selected_column_mut();
|
||||||
column.tasks.get_mut(column.selected_task_idx)
|
column.tasks.get_mut(column.selected_task_idx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_previous_task(&mut self) -> Result<(), Error> {
|
/// Selects the [`Task`] above the current one. Does nothing if
|
||||||
|
/// it's the first task on the list
|
||||||
|
pub fn select_task_above(&mut self) -> Result<(), Error> {
|
||||||
let column = self.get_selected_column_mut();
|
let column = self.get_selected_column_mut();
|
||||||
column.selected_task_idx = column.selected_task_idx.saturating_sub(1);
|
column.selected_task_idx = column.selected_task_idx.saturating_sub(1);
|
||||||
|
|
||||||
@ -144,7 +194,9 @@ impl<'a> State<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_next_task(&mut self) -> Result<(), Error> {
|
/// Selects the [`Task`] below the current one. Does nothing if
|
||||||
|
/// it's the last task on the list
|
||||||
|
pub fn select_task_below(&mut self) -> Result<(), Error> {
|
||||||
let column = self.get_selected_column_mut();
|
let column = self.get_selected_column_mut();
|
||||||
column.selected_task_idx = min(
|
column.selected_task_idx = min(
|
||||||
column.selected_task_idx + 1,
|
column.selected_task_idx + 1,
|
||||||
@ -158,6 +210,8 @@ impl<'a> State<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Selects the [`Task`] at the beginning of the list, no matter
|
||||||
|
/// where you are in the current [`Column`].
|
||||||
pub fn select_first_task(&mut self) -> Result<(), Error> {
|
pub fn select_first_task(&mut self) -> Result<(), Error> {
|
||||||
let column = self.get_selected_column_mut();
|
let column = self.get_selected_column_mut();
|
||||||
column.selected_task_idx = 0;
|
column.selected_task_idx = 0;
|
||||||
@ -169,6 +223,8 @@ impl<'a> State<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Selects the [`Task`] at the end of the list, no matter
|
||||||
|
/// where you are in the current [`Column`].
|
||||||
pub fn select_last_task(&mut self) -> Result<(), Error> {
|
pub fn select_last_task(&mut self) -> Result<(), Error> {
|
||||||
let column = self.get_selected_column_mut();
|
let column = self.get_selected_column_mut();
|
||||||
column.selected_task_idx = column.tasks.len().saturating_sub(1);
|
column.selected_task_idx = column.tasks.len().saturating_sub(1);
|
||||||
@ -180,6 +236,9 @@ impl<'a> State<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper method to construct a [`TaskState`]. Used when we are
|
||||||
|
/// going to edit an existing [`Task`]. Returns `None` if the
|
||||||
|
/// [`Column`] is empty.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn get_task_state_from_current(&self) -> Option<TaskState<'a>> {
|
pub fn get_task_state_from_current(&self) -> Option<TaskState<'a>> {
|
||||||
self.get_selected_task().map(|t| TaskState {
|
self.get_selected_task().map(|t| TaskState {
|
||||||
@ -190,20 +249,25 @@ impl<'a> State<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves the current [`Task`] up the list towards the top. Does
|
||||||
|
/// nothing if it's the first task.
|
||||||
pub fn move_task_up(&mut self) -> Result<(), Error> {
|
pub fn move_task_up(&mut self) -> Result<(), Error> {
|
||||||
self.move_task(false)
|
self.move_task(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves the current [`Task`] down the list towards the bottom. Does
|
||||||
|
/// nothing if it's the last task.
|
||||||
pub fn move_task_down(&mut self) -> Result<(), Error> {
|
pub fn move_task_down(&mut self) -> Result<(), Error> {
|
||||||
self.move_task(true)
|
self.move_task(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the move task down of this [`State`].
|
/// Private function to handle saving the current [`Task`]'s
|
||||||
pub fn move_task(&mut self, is_down: bool) -> Result<(), Error> {
|
/// state.
|
||||||
|
fn move_task(&mut self, is_down: bool) -> Result<(), Error> {
|
||||||
let other_task = if is_down {
|
let other_task = if is_down {
|
||||||
self.get_next_task()
|
self.get_task_below()
|
||||||
} else {
|
} else {
|
||||||
self.get_previous_task()
|
self.get_task_above()
|
||||||
};
|
};
|
||||||
if let (Some(task1), Some(task2)) = (self.get_selected_task(), other_task) {
|
if let (Some(task1), Some(task2)) = (self.get_selected_task(), other_task) {
|
||||||
let t1_id = task1.id;
|
let t1_id = task1.id;
|
||||||
@ -228,11 +292,15 @@ impl<'a> State<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_task_previous_column(&mut self) -> Result<(), Error> {
|
/// Moves the current [`Task`] to the [`Column`] on the left. Does
|
||||||
|
/// nothing if it's the first column.
|
||||||
|
pub fn move_task_column_left(&mut self) -> Result<(), Error> {
|
||||||
self.move_task_to_column(false)
|
self.move_task_to_column(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_task_next_column(&mut self) -> Result<(), Error> {
|
/// Moves the current [`Task`] to the [`Column`] on the right. Does
|
||||||
|
/// nothing if it's the last column.
|
||||||
|
pub fn move_task_column_right(&mut self) -> Result<(), Error> {
|
||||||
self.move_task_to_column(true)
|
self.move_task_to_column(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,13 +317,13 @@ impl<'a> State<'a> {
|
|||||||
|
|
||||||
// Only move it if it was the last task
|
// Only move it if it was the last task
|
||||||
if first_col.selected_task_idx == first_col.tasks.len() {
|
if first_col.selected_task_idx == first_col.tasks.len() {
|
||||||
self.select_previous_task()?;
|
self.select_task_above()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if move_right {
|
if move_right {
|
||||||
self.select_next_column()?;
|
self.select_column_right()?;
|
||||||
} else {
|
} else {
|
||||||
self.select_previous_column()?;
|
self.select_column_left()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let col = self.get_selected_column_mut();
|
let col = self.get_selected_column_mut();
|
||||||
@ -269,6 +337,8 @@ impl<'a> State<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Inserts a new [`Task`] into [`Column::tasks`] at the bottom of
|
||||||
|
/// the list and saves the state to the DB.
|
||||||
pub fn add_new_task(&mut self, title: String, description: String) -> Result<(), Error> {
|
pub fn add_new_task(&mut self, title: String, description: String) -> Result<(), Error> {
|
||||||
let col_id = self.get_selected_column().id;
|
let col_id = self.get_selected_column().id;
|
||||||
let task = self.db_conn.create_new_task(title, description, col_id)?;
|
let task = self.db_conn.create_new_task(title, description, col_id)?;
|
||||||
@ -283,6 +353,8 @@ impl<'a> State<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Edits the selected [`Task`] changing only it's title and/or
|
||||||
|
/// description. Does nothing if the [`Column`] is empty.
|
||||||
pub fn edit_task(&mut self, title: String, description: String) -> Result<(), Error> {
|
pub fn edit_task(&mut self, title: String, description: String) -> Result<(), Error> {
|
||||||
if let Some(selected_task) = self.get_selected_task_mut() {
|
if let Some(selected_task) = self.get_selected_task_mut() {
|
||||||
selected_task.title = title;
|
selected_task.title = title;
|
||||||
@ -294,7 +366,8 @@ impl<'a> State<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the currently selected task from the selected column
|
/// Deletes the selected [`Task`] from the list. Does nothing if
|
||||||
|
/// the [`Column`] is empty.
|
||||||
pub fn delete_task(&mut self) -> Result<(), Error> {
|
pub fn delete_task(&mut self) -> Result<(), Error> {
|
||||||
if let Some(task) = self.get_selected_task() {
|
if let Some(task) = self.get_selected_task() {
|
||||||
let task_id = task.id;
|
let task_id = task.id;
|
||||||
@ -305,7 +378,7 @@ impl<'a> State<'a> {
|
|||||||
column.tasks.remove(task_idx);
|
column.tasks.remove(task_idx);
|
||||||
|
|
||||||
if column.selected_task_idx >= column.tasks.len() {
|
if column.selected_task_idx >= column.tasks.len() {
|
||||||
self.select_previous_task()?;
|
self.select_task_above()?;
|
||||||
task_idx = task_idx.saturating_sub(1);
|
task_idx = task_idx.saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
99
src/db.rs
99
src/db.rs
@ -3,7 +3,13 @@ use anyhow::Error;
|
|||||||
use rusqlite::{params, Connection, Result};
|
use rusqlite::{params, Connection, Result};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
|
||||||
pub struct DBConn(pub Connection);
|
/// Simple one field struct to wrap a [`rusqlite::Connection`] so we
|
||||||
|
/// can assign our own methods.
|
||||||
|
pub struct DBConn(
|
||||||
|
/// Unfortunately remains public for now so we can do integration tests
|
||||||
|
/// and reuse to simulate exiting and relaunching the app.
|
||||||
|
pub Connection
|
||||||
|
);
|
||||||
|
|
||||||
impl DerefMut for DBConn {
|
impl DerefMut for DBConn {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
@ -24,22 +30,21 @@ impl DBConn {
|
|||||||
DBConn(conn)
|
DBConn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// .
|
/// Query tasks in a [`Column`] by using the column's [`Column::id`].
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// This function will return an error if something is wrong with the SQL
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn get_tasks_by_column(&self, column_name: &String) -> Result<Vec<Task>> {
|
pub fn get_tasks_by_column(&self, column_id: i64) -> Result<Vec<Task>> {
|
||||||
let mut stmt = self.prepare(
|
let mut stmt = self.prepare(
|
||||||
r#"
|
r#"
|
||||||
select task.id, title, description from task
|
select task.id, title, description from task
|
||||||
join kb_column on column_id = kb_column.id
|
where column_id = ?1
|
||||||
where kb_column.name = ?1
|
|
||||||
order by sort_order
|
order by sort_order
|
||||||
"#,
|
"#,
|
||||||
)?;
|
)?;
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
let rows = stmt.query_map([column_name], |row| {
|
let rows = stmt.query_map([column_id], |row| {
|
||||||
Ok(Task {
|
Ok(Task {
|
||||||
id: row.get(0)?,
|
id: row.get(0)?,
|
||||||
title: row.get(1)?,
|
title: row.get(1)?,
|
||||||
@ -52,20 +57,22 @@ impl DBConn {
|
|||||||
Ok(tasks)
|
Ok(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// .
|
/// Uses [get_tasks_by_column][`DBConn::get_tasks_by_column`] over
|
||||||
|
/// a loop to get all [`Column`] populated with the vec of.
|
||||||
|
/// [`Task`]
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// This function will return an error if there are issues with the SQL
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn get_all_columns(&self) -> Result<Vec<Column>> {
|
pub fn get_all_columns(&self) -> Result<Vec<Column>> {
|
||||||
let mut stmt = self.prepare("select id, name, selected_task from kb_column")?;
|
let mut stmt = self.prepare("select id, name, selected_task from kb_column")?;
|
||||||
let columns = stmt
|
let columns = stmt
|
||||||
.query_map((), |row| {
|
.query_map((), |row| {
|
||||||
let name = row.get(1)?;
|
let id = row.get(0)?;
|
||||||
Ok(Column {
|
Ok(Column {
|
||||||
id: row.get(0)?,
|
id,
|
||||||
tasks: self.get_tasks_by_column(&name)?,
|
tasks: self.get_tasks_by_column(id)?,
|
||||||
name,
|
name: row.get(1)?,
|
||||||
selected_task_idx: row.get(2)?,
|
selected_task_idx: row.get(2)?,
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
@ -74,11 +81,12 @@ impl DBConn {
|
|||||||
Ok(columns)
|
Ok(columns)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// .
|
/// Insert a new task into the DB given a title and description,
|
||||||
|
/// then return the [`Task`] with the ID provided by the DB.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn create_new_task(
|
pub fn create_new_task(
|
||||||
&self,
|
&self,
|
||||||
title: String,
|
title: String,
|
||||||
@ -102,22 +110,22 @@ impl DBConn {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// .
|
/// Deletes a [`Task`] given it's ID.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn delete_task(&self, task_id: i64) -> Result<()> {
|
pub fn delete_task(&self, task_id: i64) -> Result<()> {
|
||||||
let mut stmt = self.prepare("delete from task where id = ?1")?;
|
let mut stmt = self.prepare("delete from task where id = ?1")?;
|
||||||
stmt.execute([task_id])?;
|
stmt.execute([task_id])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// .
|
/// Updates an existing [`Task`]'s `title` and `description`.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn update_task_text(&self, task: &Task) -> Result<()> {
|
pub fn update_task_text(&self, task: &Task) -> Result<()> {
|
||||||
let mut stmt =
|
let mut stmt =
|
||||||
self.prepare("update task set title = ?2, description = ?3 where id = ?1")?;
|
self.prepare("update task set title = ?2, description = ?3 where id = ?1")?;
|
||||||
@ -125,11 +133,11 @@ impl DBConn {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// .
|
/// Moves a [`Task`] to the target [`Column`] and updates the sorting order.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn move_task_to_column(&self, task: &Task, target_column: &Column) -> Result<()> {
|
pub fn move_task_to_column(&self, task: &Task, target_column: &Column) -> Result<()> {
|
||||||
let mut stmt = self
|
let mut stmt = self
|
||||||
.prepare(
|
.prepare(
|
||||||
@ -148,7 +156,7 @@ impl DBConn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// This is a helper function in case we need to debug sort_order, because I ran into
|
/// This is a helper function in case we need to debug sort_order, because I ran into
|
||||||
/// a bug when I forgot to insert the sort_order when creating a task
|
/// a bug when I forgot to insert the sort_order when creating a task.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn get_sort_order(&self) -> Result<Vec<(i32, String, usize)>> {
|
fn get_sort_order(&self) -> Result<Vec<(i32, String, usize)>> {
|
||||||
let mut stmt = self.prepare(
|
let mut stmt = self.prepare(
|
||||||
@ -162,11 +170,14 @@ impl DBConn {
|
|||||||
Ok(tasks)
|
Ok(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// .
|
/// The order of a [`Task`] in a [`Column`] needs to be saved to
|
||||||
|
/// the DB because SQLite doesn't have a way to handle the
|
||||||
|
/// ordering the internal [`Vec<Task>`] has. This takes the
|
||||||
|
/// current sorting order of two tasks and swaps them.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn swap_task_order(&mut self, task1_id: i64, task2_id: i64) -> Result<()> {
|
pub fn swap_task_order(&mut self, task1_id: i64, task2_id: i64) -> Result<()> {
|
||||||
let tx = self.transaction()?;
|
let tx = self.transaction()?;
|
||||||
|
|
||||||
@ -194,9 +205,13 @@ impl DBConn {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Panics
|
/// Saves the currently selected column's index to `app_state` so
|
||||||
|
/// when the user reloads the project, they start on the
|
||||||
|
/// [`Column`] they were last on.
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn set_selected_column(&self, column_id: usize) -> Result<(), Error> {
|
pub fn set_selected_column(&self, column_id: usize) -> Result<(), Error> {
|
||||||
let mut stmt =
|
let mut stmt =
|
||||||
self.prepare("insert or replace into app_state(key, value) values (?1, ?2)")?;
|
self.prepare("insert or replace into app_state(key, value) values (?1, ?2)")?;
|
||||||
@ -204,9 +219,11 @@ impl DBConn {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Panics
|
/// Get's the user's last selected [`Column`] before exiting.
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn get_selected_column(&self) -> Result<usize> {
|
pub fn get_selected_column(&self) -> Result<usize> {
|
||||||
let mut stmt = self.prepare("select value from app_state where key = ?1")?;
|
let mut stmt = self.prepare("select value from app_state where key = ?1")?;
|
||||||
stmt.query_row(["selected_column"], |row| {
|
stmt.query_row(["selected_column"], |row| {
|
||||||
@ -216,18 +233,26 @@ impl DBConn {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Panics
|
/// Saves the index currently selected [`Task`] in a [`Column`] so
|
||||||
|
/// when the user reloads the project, each column selects the has
|
||||||
|
/// the last selected task before switching to another column or
|
||||||
|
/// exiting the app.
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn set_selected_task_for_column(&self, task_idx: usize, column_id: i64) -> Result<()> {
|
pub fn set_selected_task_for_column(&self, task_idx: usize, column_id: i64) -> Result<()> {
|
||||||
let mut stmt = self.prepare("update kb_column set selected_task = ?2 where id = ?1")?;
|
let mut stmt = self.prepare("update kb_column set selected_task = ?2 where id = ?1")?;
|
||||||
stmt.execute((column_id, task_idx))?;
|
stmt.execute((column_id, task_idx))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Panics
|
/// Get's each [`Column`]'s 's last selected [`Task`] before
|
||||||
|
/// switching or exiting.
|
||||||
///
|
///
|
||||||
/// Panics if something goes wrong with the SQL
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if something is wrong with the SQL.
|
||||||
pub fn get_selected_task_for_column(&self, column_id: i32) -> Result<usize> {
|
pub fn get_selected_task_for_column(&self, column_id: i32) -> Result<usize> {
|
||||||
let mut stmt = self.prepare("select selected_task from kb_column where key = ?1")?;
|
let mut stmt = self.prepare("select selected_task from kb_column where key = ?1")?;
|
||||||
stmt.query_row([column_id], |row| row.get(0))
|
stmt.query_row([column_id], |row| row.get(0))
|
||||||
|
30
src/input.rs
30
src/input.rs
@ -5,12 +5,11 @@ use crossterm::event::{Event, KeyCode};
|
|||||||
use int_enum::IntEnum;
|
use int_enum::IntEnum;
|
||||||
|
|
||||||
pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error> {
|
pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error> {
|
||||||
let cycle;
|
let cycle = if forward {
|
||||||
if forward {
|
(task.focus.int_value() + 1) % EDIT_WINDOW_FOCUS_STATES
|
||||||
cycle = (task.focus.int_value() + 1) % EDIT_WINDOW_FOCUS_STATES;
|
|
||||||
} else {
|
} else {
|
||||||
cycle = (task.focus.int_value() - 1) % EDIT_WINDOW_FOCUS_STATES;
|
(task.focus.int_value() - 1) % EDIT_WINDOW_FOCUS_STATES
|
||||||
}
|
};
|
||||||
task.focus = TaskEditFocus::from_int(cycle)?;
|
task.focus = TaskEditFocus::from_int(cycle)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -66,14 +65,14 @@ pub fn handle_task_edit(state: &mut State<'_>, key: event::KeyEvent) -> Result<(
|
|||||||
pub fn handle_main(state: &mut State<'_>, key: event::KeyEvent) -> Result<(), Error> {
|
pub fn handle_main(state: &mut State<'_>, key: event::KeyEvent) -> Result<(), Error> {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Char('q') => Ok(state.quit = true),
|
KeyCode::Char('q') => Ok(state.quit = true),
|
||||||
KeyCode::Char('h') | KeyCode::Left => state.select_previous_column(),
|
KeyCode::Char('h') | KeyCode::Left => state.select_column_left(),
|
||||||
KeyCode::Char('j') | KeyCode::Down => state.select_next_task(),
|
KeyCode::Char('j') | KeyCode::Down => state.select_task_below(),
|
||||||
KeyCode::Char('k') | KeyCode::Up => state.select_previous_task(),
|
KeyCode::Char('k') | KeyCode::Up => state.select_task_above(),
|
||||||
KeyCode::Char('l') | KeyCode::Right => state.select_next_column(),
|
KeyCode::Char('l') | KeyCode::Right => state.select_column_right(),
|
||||||
KeyCode::Char('g') => state.select_first_task(),
|
KeyCode::Char('g') => state.select_first_task(),
|
||||||
KeyCode::Char('G') => state.select_last_task(),
|
KeyCode::Char('G') => state.select_last_task(),
|
||||||
KeyCode::Char('H') => state.move_task_previous_column(),
|
KeyCode::Char('H') => state.move_task_column_left(),
|
||||||
KeyCode::Char('L') => state.move_task_next_column(),
|
KeyCode::Char('L') => state.move_task_column_right(),
|
||||||
KeyCode::Char('J') => state.move_task_down(),
|
KeyCode::Char('J') => state.move_task_down(),
|
||||||
KeyCode::Char('K') => state.move_task_up(),
|
KeyCode::Char('K') => state.move_task_up(),
|
||||||
KeyCode::Char('n') => Ok(state.task_edit_state = Some(TaskState::default())),
|
KeyCode::Char('n') => Ok(state.task_edit_state = Some(TaskState::default())),
|
||||||
@ -83,10 +82,15 @@ pub fn handle_main(state: &mut State<'_>, key: event::KeyEvent) -> Result<(), Er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Takes the app's [`State`] and uses [`event::read`] to get the current keypress.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Crossterm `event::read()` might return an error
|
/// Most of the applications errors will be bubbled up to this layer,
|
||||||
pub fn handle(state: &mut State<'_>) -> Result<(), Error> {
|
/// including all database related ones
|
||||||
|
///
|
||||||
|
/// Crossterm `event::read()` might return an error,
|
||||||
|
pub fn handle_user_keypress(state: &mut State<'_>) -> Result<(), Error> {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
if state.task_edit_state.is_some() {
|
if state.task_edit_state.is_some() {
|
||||||
handle_task_edit(state, key)?;
|
handle_task_edit(state, key)?;
|
||||||
|
18
src/lib.rs
18
src/lib.rs
@ -1,3 +1,17 @@
|
|||||||
|
//! Manage your TODOs with kanban columns right from the terminal.
|
||||||
|
//!
|
||||||
|
//! kanban-tui is a TUI based application using [`ratatui`] for
|
||||||
|
//! rendering the UI and capturing user input interaction, with
|
||||||
|
//! [`Crossterm`] as the lower-level backend system for terminal
|
||||||
|
//! text-based interfaces. The data is saved to a SQLite database
|
||||||
|
//! ideally placed in the root of your project. For this the
|
||||||
|
//! [`rusqlite`] crate provides the bindings to handle all the data
|
||||||
|
//! persistence.
|
||||||
|
//!
|
||||||
|
//! [`ratatui`]: https://crates.io/crates/ratatui
|
||||||
|
//! [`Crossterm`]: https://crates.io/crates/crossterm
|
||||||
|
//! [`rusqlite`]: https://crates.io/crates/rusqlite
|
||||||
|
|
||||||
#![deny(rust_2018_idioms)]
|
#![deny(rust_2018_idioms)]
|
||||||
mod app;
|
mod app;
|
||||||
mod db;
|
mod db;
|
||||||
@ -6,5 +20,5 @@ mod ui;
|
|||||||
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use db::*;
|
pub use db::*;
|
||||||
pub use input::handle;
|
pub use input::handle_user_keypress;
|
||||||
pub use ui::draw;
|
pub use ui::draw_ui_from_state;
|
||||||
|
@ -49,8 +49,8 @@ fn main() -> anyhow::Result<(), Box<dyn Error>> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
while !state.quit {
|
while !state.quit {
|
||||||
terminal.draw(|f| kanban_tui::draw(f, &mut state))?;
|
terminal.draw(|f| kanban_tui::draw_ui_from_state(f, &mut state))?;
|
||||||
kanban_tui::handle(&mut state)?;
|
kanban_tui::handle_user_keypress(&mut state)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore terminal
|
// restore terminal
|
||||||
|
@ -216,14 +216,16 @@ fn draw_project_stats<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &mut
|
|||||||
f.render_widget(list, area);
|
f.render_widget(list, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Macro to generate keybindings string at compile time
|
/// Macro to generate the app's keybindings string at compile time
|
||||||
macro_rules! unroll {
|
macro_rules! unroll {
|
||||||
(($first_a:literal, $first_b:literal), $(($a:literal, $b:literal)),*) => {
|
(($first_a:literal, $first_b:literal), $(($a:literal, $b:literal)),*) => {
|
||||||
concat!(concat!($first_a, ": ", $first_b) $(," | ", concat!($a, ": ", $b))*)
|
concat!(concat!($first_a, ": ", $first_b) $(," | ", concat!($a, ": ", $b))*)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>) {
|
/// Takes the app's [`State`] so [ratatui][`tui`] can render it to the
|
||||||
|
/// terminal screen
|
||||||
|
pub fn draw_ui_from_state<B: Backend>(f: &mut Frame<'_, B>, state: &mut State<'_>) {
|
||||||
let main_layout = Layout::default()
|
let main_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(
|
.constraints(
|
||||||
|
@ -25,10 +25,10 @@ mod app_tests {
|
|||||||
|
|
||||||
state.add_new_task(String::from("T1"), String::from("D1"))?;
|
state.add_new_task(String::from("T1"), String::from("D1"))?;
|
||||||
state.add_new_task(String::from("T2"), String::from("D2"))?;
|
state.add_new_task(String::from("T2"), String::from("D2"))?;
|
||||||
state.select_next_column()?;
|
state.select_column_right()?;
|
||||||
state.select_next_column()?;
|
state.select_column_right()?;
|
||||||
state.add_new_task(String::from("T3"), String::from("D3"))?;
|
state.add_new_task(String::from("T3"), String::from("D3"))?;
|
||||||
state.select_previous_column()?;
|
state.select_column_left()?;
|
||||||
state.add_new_task(String::from("T4"), String::from("D4"))?;
|
state.add_new_task(String::from("T4"), String::from("D4"))?;
|
||||||
|
|
||||||
assert_eq!(state.columns[0].tasks.len(), 2);
|
assert_eq!(state.columns[0].tasks.len(), 2);
|
||||||
@ -63,33 +63,33 @@ mod app_tests {
|
|||||||
state.move_task_down()?;
|
state.move_task_down()?;
|
||||||
state.move_task_down()?;
|
state.move_task_down()?;
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
state.select_next_column()?;
|
state.select_column_right()?;
|
||||||
}
|
}
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
state.select_previous_column()?;
|
state.select_column_left()?;
|
||||||
}
|
}
|
||||||
state.add_new_task(String::from("T1"), String::from("D1"))?;
|
state.add_new_task(String::from("T1"), String::from("D1"))?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
||||||
state.add_new_task(String::from("T2"), String::from("D2"))?;
|
state.add_new_task(String::from("T2"), String::from("D2"))?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
||||||
for _ in 0..6 {
|
for _ in 0..6 {
|
||||||
state.select_next_column()?;
|
state.select_column_right()?;
|
||||||
}
|
}
|
||||||
state.select_previous_column()?;
|
state.select_column_left()?;
|
||||||
state.add_new_task(String::from("T3"), String::from("D3"))?;
|
state.add_new_task(String::from("T3"), String::from("D3"))?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
assert_eq!(state.get_selected_column().name, "Done");
|
assert_eq!(state.get_selected_column().name, "Done");
|
||||||
for _ in 0..6 {
|
for _ in 0..6 {
|
||||||
state.select_next_column()?;
|
state.select_column_right()?;
|
||||||
}
|
}
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
state.select_previous_column()?;
|
state.select_column_left()?;
|
||||||
}
|
}
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
||||||
state.select_next_column()?;
|
state.select_column_right()?;
|
||||||
state.select_next_column()?;
|
state.select_column_right()?;
|
||||||
|
|
||||||
|
|
||||||
// Reload the data from the database then rerun the asserts to
|
// Reload the data from the database then rerun the asserts to
|
||||||
@ -97,13 +97,13 @@ mod app_tests {
|
|||||||
let mut state = State::new(state.db_conn.0)?;
|
let mut state = State::new(state.db_conn.0)?;
|
||||||
|
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
state.select_next_task()?;
|
state.select_task_below()?;
|
||||||
state.select_next_task()?;
|
state.select_task_below()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
state.select_previous_column()?;
|
state.select_column_left()?;
|
||||||
state.select_previous_column()?;
|
state.select_column_left()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
||||||
state.select_next_task()?;
|
state.select_task_below()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -128,8 +128,8 @@ mod app_tests {
|
|||||||
state.select_first_task()?;
|
state.select_first_task()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
state.select_last_task()?;
|
state.select_last_task()?;
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T9");
|
assert_eq!(state.get_selected_task().unwrap().title, "T9");
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
state.move_task_up()?;
|
state.move_task_up()?;
|
||||||
@ -160,24 +160,24 @@ mod app_tests {
|
|||||||
state.add_new_task(String::from("T1"), String::from("D1"))?;
|
state.add_new_task(String::from("T1"), String::from("D1"))?;
|
||||||
state.add_new_task(String::from("T2"), String::from("D2"))?;
|
state.add_new_task(String::from("T2"), String::from("D2"))?;
|
||||||
|
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
state.move_task_up()?;
|
state.move_task_up()?;
|
||||||
state.move_task_down()?;
|
state.move_task_down()?;
|
||||||
state.move_task_down()?;
|
state.move_task_down()?;
|
||||||
|
|
||||||
assert_eq!(&state.columns[0].tasks[1].title, "T1");
|
assert_eq!(&state.columns[0].tasks[1].title, "T1");
|
||||||
assert_eq!(&state.columns[0].tasks[0].title, "T2");
|
assert_eq!(&state.columns[0].tasks[0].title, "T2");
|
||||||
state.select_next_column()?;
|
state.select_column_right()?;
|
||||||
state.add_new_task(String::from("T3"), String::from("D3"))?;
|
state.add_new_task(String::from("T3"), String::from("D3"))?;
|
||||||
state.move_task_next_column()?;
|
state.move_task_column_right()?;
|
||||||
assert_eq!(state.columns[1].tasks.len(), 0);
|
assert_eq!(state.columns[1].tasks.len(), 0);
|
||||||
assert_eq!(state.columns[2].tasks.len(), 1);
|
assert_eq!(state.columns[2].tasks.len(), 1);
|
||||||
|
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
state.move_task_next_column()?;
|
state.move_task_column_right()?;
|
||||||
}
|
}
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
state.move_task_previous_column()?;
|
state.move_task_column_left()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(state.columns[0].tasks.len(), 3);
|
assert_eq!(state.columns[0].tasks.len(), 3);
|
||||||
@ -185,25 +185,25 @@ mod app_tests {
|
|||||||
assert_eq!(state.columns[2].tasks.len(), 0);
|
assert_eq!(state.columns[2].tasks.len(), 0);
|
||||||
assert_eq!(state.columns[3].tasks.len(), 0);
|
assert_eq!(state.columns[3].tasks.len(), 0);
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
state.select_next_task()?;
|
state.select_task_below()?;
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
state.select_first_task()?;
|
state.select_first_task()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
state.select_last_task()?;
|
state.select_last_task()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
|
|
||||||
// Reload the data from the database then rerun the asserts to
|
// Reload the data from the database then rerun the asserts to
|
||||||
// make sure everything was saved correctly
|
// make sure everything was saved correctly
|
||||||
let mut state = State::new(state.db_conn.0)?;
|
let mut state = State::new(state.db_conn.0)?;
|
||||||
|
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
||||||
state.select_next_task()?;
|
state.select_task_below()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
state.select_next_task()?;
|
state.select_task_below()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
state.select_first_task()?;
|
state.select_first_task()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
@ -231,7 +231,7 @@ mod app_tests {
|
|||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
assert_eq!(state.get_selected_task().unwrap().description, "D1");
|
assert_eq!(state.get_selected_task().unwrap().description, "D1");
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
state.move_task_next_column()?;
|
state.move_task_column_right()?;
|
||||||
}
|
}
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
assert_eq!(state.get_selected_task().unwrap().description, "D1");
|
assert_eq!(state.get_selected_task().unwrap().description, "D1");
|
||||||
@ -239,7 +239,7 @@ mod app_tests {
|
|||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
assert_eq!(state.get_selected_task().unwrap().description, "D3");
|
assert_eq!(state.get_selected_task().unwrap().description, "D3");
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
state.move_task_previous_column()?;
|
state.move_task_column_left()?;
|
||||||
}
|
}
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
assert_eq!(state.get_selected_task().unwrap().description, "D3");
|
assert_eq!(state.get_selected_task().unwrap().description, "D3");
|
||||||
@ -283,10 +283,10 @@ mod app_tests {
|
|||||||
state.delete_task()?;
|
state.delete_task()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
state.add_new_task(String::from("T3"), String::from("D3"))?;
|
state.add_new_task(String::from("T3"), String::from("D3"))?;
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
assert_eq!(state.get_selected_task().unwrap().title, "T2");
|
||||||
state.delete_task()?;
|
state.delete_task()?;
|
||||||
state.select_previous_task()?;
|
state.select_task_above()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
||||||
state.select_last_task()?;
|
state.select_last_task()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
@ -296,7 +296,7 @@ mod app_tests {
|
|||||||
}
|
}
|
||||||
state.delete_task()?;
|
state.delete_task()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
assert_eq!(state.get_selected_task().unwrap().title, "T1");
|
||||||
state.select_next_task()?;
|
state.select_task_below()?;
|
||||||
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
assert_eq!(state.get_selected_task().unwrap().title, "T3");
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
state.delete_task()?;
|
state.delete_task()?;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user