From 4beda4fae788c2cb050e9ef609fa0826dcc5bd11 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 17 Jun 2023 22:47:54 +0700 Subject: [PATCH] Add docs to everything public. Rename some methods.Remove unused deps --- Cargo.lock | 68 +-------------------------- Cargo.toml | 5 +- src/app.rs | 113 +++++++++++++++++++++++++++++++++++++-------- src/db.rs | 99 ++++++++++++++++++++++++--------------- src/input.rs | 30 ++++++------ src/lib.rs | 18 +++++++- src/main.rs | 4 +- src/ui.rs | 6 ++- tests/app_tests.rs | 70 ++++++++++++++-------------- 9 files changed, 231 insertions(+), 182 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cffa55..28a3c7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 868a858..95d767c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" ] } \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index e13996d..1b12ab3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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, } -#[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, + /// 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>, } @@ -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> { 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); } diff --git a/src/db.rs b/src/db.rs index fab3493..7f52f5f 100644 --- a/src/db.rs +++ b/src/db.rs @@ -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> { + /// Returns an error if something is wrong with the SQL. + pub fn get_tasks_by_column(&self, column_id: i64) -> Result> { 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> { 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> { 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`] 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 { 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 { let mut stmt = self.prepare("select selected_task from kb_column where key = ?1")?; stmt.query_row([column_id], |row| row.get(0)) diff --git a/src/input.rs b/src/input.rs index 30dde92..0e2ac03 100644 --- a/src/input.rs +++ b/src/input.rs @@ -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)?; diff --git a/src/lib.rs b/src/lib.rs index 0e6f329..f5cdd37 100644 --- a/src/lib.rs +++ b/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)] 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; diff --git a/src/main.rs b/src/main.rs index 40605fc..d94edae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,8 +49,8 @@ fn main() -> anyhow::Result<(), Box> { 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 diff --git a/src/ui.rs b/src/ui.rs index 314a743..e03bb35 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -216,14 +216,16 @@ fn draw_project_stats(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(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(f: &mut Frame<'_, B>, state: &mut State<'_>) { let main_layout = Layout::default() .direction(Direction::Vertical) .constraints( diff --git a/tests/app_tests.rs b/tests/app_tests.rs index 9bccf38..0ab6b87 100644 --- a/tests/app_tests.rs +++ b/tests/app_tests.rs @@ -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()?;