Add docs to everything public. Rename some methods.Remove unused deps

This commit is contained in:
Joseph Ferano 2023-06-17 22:47:54 +07:00
parent 9a779b1abb
commit 4beda4fae7
9 changed files with 231 additions and 182 deletions

68
Cargo.lock generated
View File

@ -301,13 +301,7 @@ dependencies = [
]
[[package]]
name = "itoa"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
[[package]]
name = "kanban-tui"
name = "kanban_tui"
version = "0.1.0"
dependencies = [
"anyhow",
@ -316,9 +310,6 @@ dependencies = [
"int-enum",
"ratatui",
"rusqlite",
"serde",
"serde_json",
"thiserror",
"tui-textarea",
]
@ -492,49 +483,12 @@ dependencies = [
"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]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "signal-hook"
version = "0.3.15"
@ -599,26 +553,6 @@ dependencies = [
"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]]
name = "toml_datetime"
version = "0.6.2"

View File

@ -1,5 +1,5 @@
[package]
name = "kanban-tui"
name = "kanban_tui"
version = "0.1.0"
edition = "2021"
@ -9,10 +9,7 @@ edition = "2021"
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 }
crossterm = "0.26.1"
serde = { version = "1.0.148" , features = [ "derive" ] }
serde_json = "1.0.89"
int-enum = "0.5.0"
thiserror = "1"
anyhow = "1"
clap = { version = "4.3.2" , features = [ "derive" ] }
rusqlite = { version = "0.29", features = [ "bundled" ] }

View File

@ -1,42 +1,65 @@
use anyhow::Error;
use int_enum::IntEnum;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::cmp::min;
use tui_textarea::TextArea;
use crate::db;
#[derive(Debug, Serialize, Deserialize)]
/// Represents a kanban column containing the tasks and other metadata.
#[derive(Debug)]
pub struct Column {
/// Id provided by the database
pub id: i64,
/// The name used for the title in the UI
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,
/// The collection of [`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 {
/// Id provided by the database
pub id: i64,
/// Title of the [`Task`]
pub title: String,
/// Description of the [`Task`]
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;
/// Used to track the focus of the form field in the task edit window.
#[repr(i8)]
#[derive(Debug, IntEnum, Copy, Clone)]
pub enum TaskEditFocus {
/// Title text input line
Title = 0,
/// Description text input box
Description = 1,
/// Confirm changes button
ConfirmBtn = 2,
/// Cancel changes button
CancelBtn = 3,
}
/// Represents the transient state of a task while it is being editing
/// by the user through the UI.
pub struct TaskState<'a> {
/// The title of the Task
pub title: TextArea<'a>,
/// The description of the Task
pub description: TextArea<'a>,
/// Where the current focus of the task edit form is
pub focus: TaskEditFocus,
/// Used to decide if the user is editing an existing task or
/// creating a new one
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> {
/// The name of the project, currently derived from the name of
/// the current working directory
pub project_name: String,
/// The index of the currently selected [`Column`]
pub selected_column_idx: usize,
/// A vec of all the [`Column`]s
pub columns: Vec<Column>,
/// The [`db::DBConn`] wrapping a [`rusqlite::Connection`]
pub db_conn: db::DBConn,
/// Flag to check on each loop whether we should exit the app
pub quit: bool,
/// If [`Some(TaskState)`] then we are in the task edit form window
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]
pub fn get_selected_column(&self) -> &Column {
&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 {
&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.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.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]
pub fn get_selected_task(&self) -> Option<&Task> {
let column = self.get_selected_column();
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]
pub fn get_previous_task(&self) -> Option<&Task> {
pub fn get_task_above(&self) -> Option<&Task> {
let column = self.get_selected_column();
if column.selected_task_idx > 0 {
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]
pub fn get_next_task(&self) -> Option<&Task> {
pub fn get_task_below(&self) -> Option<&Task> {
let column = self.get_selected_column();
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> {
let column = self.get_selected_column_mut();
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();
column.selected_task_idx = column.selected_task_idx.saturating_sub(1);
@ -144,7 +194,9 @@ impl<'a> State<'a> {
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();
column.selected_task_idx = min(
column.selected_task_idx + 1,
@ -158,6 +210,8 @@ impl<'a> State<'a> {
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> {
let column = self.get_selected_column_mut();
column.selected_task_idx = 0;
@ -169,6 +223,8 @@ impl<'a> State<'a> {
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> {
let column = self.get_selected_column_mut();
column.selected_task_idx = column.tasks.len().saturating_sub(1);
@ -180,6 +236,9 @@ impl<'a> State<'a> {
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]
pub fn get_task_state_from_current(&self) -> Option<TaskState<'a>> {
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> {
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> {
self.move_task(true)
}
/// Returns the move task down of this [`State`].
pub fn move_task(&mut self, is_down: bool) -> Result<(), Error> {
/// Private function to handle saving the current [`Task`]'s
/// state.
fn move_task(&mut self, is_down: bool) -> Result<(), Error> {
let other_task = if is_down {
self.get_next_task()
self.get_task_below()
} else {
self.get_previous_task()
self.get_task_above()
};
if let (Some(task1), Some(task2)) = (self.get_selected_task(), other_task) {
let t1_id = task1.id;
@ -228,11 +292,15 @@ impl<'a> State<'a> {
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)
}
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)
}
@ -249,13 +317,13 @@ impl<'a> State<'a> {
// Only move it if it was the last task
if first_col.selected_task_idx == first_col.tasks.len() {
self.select_previous_task()?;
self.select_task_above()?;
}
if move_right {
self.select_next_column()?;
self.select_column_right()?;
} else {
self.select_previous_column()?;
self.select_column_left()?;
}
let col = self.get_selected_column_mut();
@ -269,6 +337,8 @@ impl<'a> State<'a> {
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> {
let col_id = self.get_selected_column().id;
let task = self.db_conn.create_new_task(title, description, col_id)?;
@ -283,6 +353,8 @@ impl<'a> State<'a> {
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> {
if let Some(selected_task) = self.get_selected_task_mut() {
selected_task.title = title;
@ -294,7 +366,8 @@ impl<'a> State<'a> {
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> {
if let Some(task) = self.get_selected_task() {
let task_id = task.id;
@ -305,7 +378,7 @@ impl<'a> State<'a> {
column.tasks.remove(task_idx);
if column.selected_task_idx >= column.tasks.len() {
self.select_previous_task()?;
self.select_task_above()?;
task_idx = task_idx.saturating_sub(1);
}

View File

@ -3,7 +3,13 @@ use anyhow::Error;
use rusqlite::{params, Connection, Result};
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 {
fn deref_mut(&mut self) -> &mut Self::Target {
@ -24,22 +30,21 @@ impl DBConn {
DBConn(conn)
}
/// .
/// Query tasks in a [`Column`] by using the column's [`Column::id`].
///
/// # Errors
///
/// This function will return an error if something is wrong with the SQL
pub fn get_tasks_by_column(&self, column_name: &String) -> Result<Vec<Task>> {
/// Returns an error if something is wrong with the SQL.
pub fn get_tasks_by_column(&self, column_id: i64) -> Result<Vec<Task>> {
let mut stmt = self.prepare(
r#"
select task.id, title, description from task
join kb_column on column_id = kb_column.id
where kb_column.name = ?1
where column_id = ?1
order by sort_order
"#,
)?;
let mut tasks = Vec::new();
let rows = stmt.query_map([column_name], |row| {
let rows = stmt.query_map([column_id], |row| {
Ok(Task {
id: row.get(0)?,
title: row.get(1)?,
@ -52,20 +57,22 @@ impl DBConn {
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
///
/// 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>> {
let mut stmt = self.prepare("select id, name, selected_task from kb_column")?;
let columns = stmt
.query_map((), |row| {
let name = row.get(1)?;
let id = row.get(0)?;
Ok(Column {
id: row.get(0)?,
tasks: self.get_tasks_by_column(&name)?,
name,
id,
tasks: self.get_tasks_by_column(id)?,
name: row.get(1)?,
selected_task_idx: row.get(2)?,
})
})?
@ -74,11 +81,12 @@ impl DBConn {
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(
&self,
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<()> {
let mut stmt = self.prepare("delete from task where id = ?1")?;
stmt.execute([task_id])?;
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<()> {
let mut stmt =
self.prepare("update task set title = ?2, description = ?3 where id = ?1")?;
@ -125,11 +133,11 @@ impl DBConn {
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<()> {
let mut stmt = self
.prepare(
@ -148,7 +156,7 @@ impl DBConn {
}
/// 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)]
fn get_sort_order(&self) -> Result<Vec<(i32, String, usize)>> {
let mut stmt = self.prepare(
@ -162,11 +170,14 @@ impl DBConn {
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<()> {
let tx = self.transaction()?;
@ -194,9 +205,13 @@ impl DBConn {
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> {
let mut stmt =
self.prepare("insert or replace into app_state(key, value) values (?1, ?2)")?;
@ -204,9 +219,11 @@ impl DBConn {
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> {
let mut stmt = self.prepare("select value from app_state where key = ?1")?;
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<()> {
let mut stmt = self.prepare("update kb_column set selected_task = ?2 where id = ?1")?;
stmt.execute((column_id, task_idx))?;
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> {
let mut stmt = self.prepare("select selected_task from kb_column where key = ?1")?;
stmt.query_row([column_id], |row| row.get(0))

View File

@ -5,12 +5,11 @@ use crossterm::event::{Event, KeyCode};
use int_enum::IntEnum;
pub fn cycle_focus(task: &mut TaskState<'_>, forward: bool) -> Result<(), Error> {
let cycle;
if forward {
cycle = (task.focus.int_value() + 1) % EDIT_WINDOW_FOCUS_STATES;
let cycle = if forward {
(task.focus.int_value() + 1) % EDIT_WINDOW_FOCUS_STATES
} 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)?;
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> {
match key.code {
KeyCode::Char('q') => Ok(state.quit = true),
KeyCode::Char('h') | KeyCode::Left => state.select_previous_column(),
KeyCode::Char('j') | KeyCode::Down => state.select_next_task(),
KeyCode::Char('k') | KeyCode::Up => state.select_previous_task(),
KeyCode::Char('l') | KeyCode::Right => state.select_next_column(),
KeyCode::Char('h') | KeyCode::Left => state.select_column_left(),
KeyCode::Char('j') | KeyCode::Down => state.select_task_below(),
KeyCode::Char('k') | KeyCode::Up => state.select_task_above(),
KeyCode::Char('l') | KeyCode::Right => state.select_column_right(),
KeyCode::Char('g') => state.select_first_task(),
KeyCode::Char('G') => state.select_last_task(),
KeyCode::Char('H') => state.move_task_previous_column(),
KeyCode::Char('L') => state.move_task_next_column(),
KeyCode::Char('H') => state.move_task_column_left(),
KeyCode::Char('L') => state.move_task_column_right(),
KeyCode::Char('J') => state.move_task_down(),
KeyCode::Char('K') => state.move_task_up(),
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
///
/// Crossterm `event::read()` might return an error
pub fn handle(state: &mut State<'_>) -> Result<(), Error> {
/// Most of the applications errors will be bubbled up to this layer,
/// 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 state.task_edit_state.is_some() {
handle_task_edit(state, key)?;

View File

@ -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)]
mod app;
mod db;
@ -6,5 +20,5 @@ mod ui;
pub use app::*;
pub use db::*;
pub use input::handle;
pub use ui::draw;
pub use input::handle_user_keypress;
pub use ui::draw_ui_from_state;

View File

@ -49,8 +49,8 @@ fn main() -> anyhow::Result<(), Box<dyn Error>> {
let mut terminal = Terminal::new(backend)?;
while !state.quit {
terminal.draw(|f| kanban_tui::draw(f, &mut state))?;
kanban_tui::handle(&mut state)?;
terminal.draw(|f| kanban_tui::draw_ui_from_state(f, &mut state))?;
kanban_tui::handle_user_keypress(&mut state)?;
}
// restore terminal

View File

@ -216,14 +216,16 @@ fn draw_project_stats<B: Backend>(f: &mut Frame<'_, B>, area: Rect, state: &mut
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 {
(($first_a:literal, $first_b:literal), $(($a:literal, $b:literal)),*) => {
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()
.direction(Direction::Vertical)
.constraints(

View File

@ -25,10 +25,10 @@ mod app_tests {
state.add_new_task(String::from("T1"), String::from("D1"))?;
state.add_new_task(String::from("T2"), String::from("D2"))?;
state.select_next_column()?;
state.select_next_column()?;
state.select_column_right()?;
state.select_column_right()?;
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"))?;
assert_eq!(state.columns[0].tasks.len(), 2);
@ -63,33 +63,33 @@ mod app_tests {
state.move_task_down()?;
state.move_task_down()?;
for _ in 0..10 {
state.select_next_column()?;
state.select_column_right()?;
}
for _ in 0..10 {
state.select_previous_column()?;
state.select_column_left()?;
}
state.add_new_task(String::from("T1"), String::from("D1"))?;
assert_eq!(state.get_selected_task().unwrap().title, "T1");
state.add_new_task(String::from("T2"), String::from("D2"))?;
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");
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"))?;
assert_eq!(state.get_selected_task().unwrap().title, "T3");
assert_eq!(state.get_selected_column().name, "Done");
for _ in 0..6 {
state.select_next_column()?;
state.select_column_right()?;
}
for _ in 0..4 {
state.select_previous_column()?;
state.select_column_left()?;
}
assert_eq!(state.get_selected_task().unwrap().title, "T1");
state.select_next_column()?;
state.select_next_column()?;
state.select_column_right()?;
state.select_column_right()?;
// 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)?;
assert_eq!(state.get_selected_task().unwrap().title, "T3");
state.select_next_task()?;
state.select_next_task()?;
state.select_task_below()?;
state.select_task_below()?;
assert_eq!(state.get_selected_task().unwrap().title, "T3");
state.select_previous_column()?;
state.select_previous_column()?;
state.select_column_left()?;
state.select_column_left()?;
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");
Ok(())
@ -128,8 +128,8 @@ mod app_tests {
state.select_first_task()?;
assert_eq!(state.get_selected_task().unwrap().title, "T2");
state.select_last_task()?;
state.select_previous_task()?;
state.select_previous_task()?;
state.select_task_above()?;
state.select_task_above()?;
assert_eq!(state.get_selected_task().unwrap().title, "T9");
for _ in 0..10 {
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("T2"), String::from("D2"))?;
state.select_previous_task()?;
state.select_task_above()?;
state.move_task_up()?;
state.move_task_down()?;
state.move_task_down()?;
assert_eq!(&state.columns[0].tasks[1].title, "T1");
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.move_task_next_column()?;
state.move_task_column_right()?;
assert_eq!(state.columns[1].tasks.len(), 0);
assert_eq!(state.columns[2].tasks.len(), 1);
for _ in 0..5 {
state.move_task_next_column()?;
state.move_task_column_right()?;
}
for _ in 0..4 {
state.move_task_previous_column()?;
state.move_task_column_left()?;
}
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[3].tasks.len(), 0);
assert_eq!(state.get_selected_task().unwrap().title, "T3");
state.select_next_task()?;
state.select_previous_task()?;
state.select_task_below()?;
state.select_task_above()?;
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");
state.select_first_task()?;
assert_eq!(state.get_selected_task().unwrap().title, "T2");
state.select_last_task()?;
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
// make sure everything was saved correctly
let mut state = State::new(state.db_conn.0)?;
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");
state.select_next_task()?;
state.select_task_below()?;
assert_eq!(state.get_selected_task().unwrap().title, "T3");
state.select_first_task()?;
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().description, "D1");
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().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().description, "D3");
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().description, "D3");
@ -283,10 +283,10 @@ mod app_tests {
state.delete_task()?;
assert_eq!(state.get_selected_task().unwrap().title, "T2");
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");
state.delete_task()?;
state.select_previous_task()?;
state.select_task_above()?;
assert_eq!(state.get_selected_task().unwrap().title, "T1");
state.select_last_task()?;
assert_eq!(state.get_selected_task().unwrap().title, "T3");
@ -296,7 +296,7 @@ mod app_tests {
}
state.delete_task()?;
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");
for _ in 0..4 {
state.delete_task()?;