From 52bdf3aa8f904855895e68f8254976ca658a317b Mon Sep 17 00:00:00 2001 From: Kenneth Love <11908+kennethlove@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:49:44 -0700 Subject: [PATCH 1/5] Deleted commented out code --- src/db.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/db.rs b/src/db.rs index be32aa0..d08d65b 100644 --- a/src/db.rs +++ b/src/db.rs @@ -13,8 +13,7 @@ lazy_static::lazy_static! { #[derive(Debug)] pub struct Database { pub filename: String, - pub streaks: Vec, // pub streaks: Arc>>, - // pub streaks: Arc>>, + pub streaks: Vec, } impl Clone for Database { From d08996eed2fe0e5061d9ccc3ef7108217bf63e00 Mon Sep 17 00:00:00 2001 From: Kenneth Love Date: Tue, 13 Aug 2024 17:30:04 -0700 Subject: [PATCH 2/5] Implemented search --- src/cli.rs | 26 +++++++++++++++++-------- src/db.rs | 53 ++++++++++++--------------------------------------- src/streak.rs | 43 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8b1895f..2a04b33 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -14,6 +14,7 @@ use crate::{ streak::{Frequency, Streak}, tui, }; +use crate::streak::sort_streaks; #[derive(Debug, Parser)] #[command(version, about, long_about = None)] @@ -30,6 +31,9 @@ enum Commands { List { #[arg(long, default_value = "task+", help = "Sort by field")] sort_by: String, + + #[arg(long, default_value = "", help = "Search for task")] + search: String, }, #[command(about = "Create a new streak", long_about = None, short_flag = 'a')] Add { @@ -50,16 +54,16 @@ enum Commands { } /// Create a new daily streak item -fn new_daily(name: String, db: &mut Database) -> Result> { - let streak = Streak::new_daily(name); +fn new_daily(task: String, db: &mut Database) -> Result> { + let streak = Streak::new_daily(task); db.streaks.push(streak.clone()); db.save()?; Ok(streak) } /// Create a new weekly streak item -fn new_weekly(name: String, db: &mut Database) -> Result> { - let streak = Streak::new_weekly(name); +fn new_weekly(task: String, db: &mut Database) -> Result> { + let streak = Streak::new_weekly(task); db.streaks.push(streak.clone()); db.save()?; Ok(streak) @@ -260,11 +264,17 @@ pub fn parse() { println!("{tada} {response} {}", streak.task); } }, - Commands::List { sort_by } => { + Commands::List { sort_by, search } => { + let mut streak_list = match search.is_empty() { + true => db.get_all().unwrap(), + false => db.search(search), + }; + // TODO: change `sort_by` to `&str` let sort_by = get_sort_order(sort_by.to_string()); - let streak_list = match sort_by { - Some((field, direction)) => db.get_sorted(field, direction), - None => db.get_all().unwrap(), + + streak_list = match sort_by { + Some((field, direction)) => sort_streaks(streak_list, field, direction), + None => streak_list, }; println!("{}", build_table(streak_list)); } diff --git a/src/db.rs b/src/db.rs index d08d65b..c860eee 100644 --- a/src/db.rs +++ b/src/db.rs @@ -3,7 +3,7 @@ use std::io::Write; use std::sync::Mutex; use crate::cli::{SortByDirection, SortByField}; -use crate::streak::Streak; +use crate::streak::{sort_streaks, Streak}; use uuid::Uuid; lazy_static::lazy_static! { @@ -144,46 +144,8 @@ impl Database { sort_field: SortByField, sort_direction: SortByDirection, ) -> Vec { - let mut streaks = self.streaks.clone(); - match (sort_field, sort_direction) { - (SortByField::Task, SortByDirection::Ascending) => { - streaks.sort_by(|a, b| a.task.cmp(&b.task)) - } - (SortByField::Task, SortByDirection::Descending) => { - streaks.sort_by(|a, b| b.task.cmp(&a.task)) - } - (SortByField::Frequency, SortByDirection::Ascending) => { - streaks.sort_by(|a, b| a.frequency.cmp(&b.frequency)) - } - (SortByField::Frequency, SortByDirection::Descending) => { - streaks.sort_by(|a, b| b.frequency.cmp(&a.frequency)) - } - (SortByField::LastCheckIn, SortByDirection::Ascending) => { - streaks.sort_by(|a, b| a.last_checkin.cmp(&b.last_checkin)) - } - (SortByField::LastCheckIn, SortByDirection::Descending) => { - streaks.sort_by(|a, b| b.last_checkin.cmp(&a.last_checkin)) - } - (SortByField::CurrentStreak, SortByDirection::Ascending) => { - streaks.sort_by(|a, b| a.current_streak.cmp(&b.current_streak)) - } - (SortByField::CurrentStreak, SortByDirection::Descending) => { - streaks.sort_by(|a, b| b.current_streak.cmp(&a.current_streak)) - } - (SortByField::LongestStreak, SortByDirection::Ascending) => { - streaks.sort_by(|a, b| a.longest_streak.cmp(&b.longest_streak)) - } - (SortByField::LongestStreak, SortByDirection::Descending) => { - streaks.sort_by(|a, b| b.longest_streak.cmp(&a.longest_streak)) - } - (SortByField::TotalCheckins, SortByDirection::Ascending) => { - streaks.sort_by(|a, b| a.total_checkins.cmp(&b.total_checkins)) - } - (SortByField::TotalCheckins, SortByDirection::Descending) => { - streaks.sort_by(|a, b| b.total_checkins.cmp(&a.total_checkins)) - } - } - streaks + let streaks = self.streaks.clone(); + sort_streaks(streaks, sort_field, sort_direction) } pub fn get_one(&mut self, id: Uuid) -> Option { @@ -212,6 +174,15 @@ impl Database { None => None, } } + + pub fn search(&mut self, query: &str) -> Vec { + let streaks = self.streaks.clone(); + streaks + .iter() + .filter(|s| s.task.contains(query)) + .cloned() + .collect() + } } impl Default for Database { diff --git a/src/streak.rs b/src/streak.rs index 77027fa..12f7485 100644 --- a/src/streak.rs +++ b/src/streak.rs @@ -5,6 +5,7 @@ use chrono::{Local, NaiveDate}; use clap::ValueEnum; use serde::{Deserialize, Serialize}; use uuid::Uuid; +use crate::cli::{SortByDirection, SortByField}; #[derive( Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd, ValueEnum, Serialize, Deserialize, @@ -177,6 +178,48 @@ impl Default for Streak { } } +pub fn sort_streaks(mut streaks: Vec, sort_field: SortByField, sort_direction: SortByDirection) -> Vec { + match (sort_field, sort_direction) { + (SortByField::Task, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.task.cmp(&b.task)) + } + (SortByField::Task, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.task.cmp(&a.task)) + } + (SortByField::Frequency, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.frequency.cmp(&b.frequency)) + } + (SortByField::Frequency, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.frequency.cmp(&a.frequency)) + } + (SortByField::LastCheckIn, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.last_checkin.cmp(&b.last_checkin)) + } + (SortByField::LastCheckIn, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.last_checkin.cmp(&a.last_checkin)) + } + (SortByField::CurrentStreak, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.current_streak.cmp(&b.current_streak)) + } + (SortByField::CurrentStreak, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.current_streak.cmp(&a.current_streak)) + } + (SortByField::LongestStreak, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.longest_streak.cmp(&b.longest_streak)) + } + (SortByField::LongestStreak, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.longest_streak.cmp(&a.longest_streak)) + } + (SortByField::TotalCheckins, SortByDirection::Ascending) => { + streaks.sort_by(|a, b| a.total_checkins.cmp(&b.total_checkins)) + } + (SortByField::TotalCheckins, SortByDirection::Descending) => { + streaks.sort_by(|a, b| b.total_checkins.cmp(&a.total_checkins)) + } + } + streaks +} + #[cfg(test)] mod tests { use chrono::{NaiveDate, TimeDelta}; From 99be0df73d60118bee04490af8fa99706481fbf4 Mon Sep 17 00:00:00 2001 From: Kenneth Love Date: Tue, 13 Aug 2024 18:15:15 -0700 Subject: [PATCH 3/5] Other stuff --- src/cli.rs | 41 ++++++++++++++++++----------------------- src/db.rs | 16 ++++++++-------- src/tui.rs | 4 +--- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 2a04b33..9a78455 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -72,10 +72,7 @@ fn new_weekly(task: String, db: &mut Database) -> Result Vec { - match db.get_all() { - Some(streaks) => streaks.clone(), - None => Vec::::new(), - } + db.get_all() } /// Get one single streak @@ -142,10 +139,12 @@ fn build_table(streaks: Vec) -> String { for streak in streaks.iter() { let mut wrapped_text = String::new(); - let wrapped_lines = textwrap::wrap(&streak.task.as_str(), 60); + let wrapped_lines = textwrap::wrap(&streak.task.as_str(), 40); for line in wrapped_lines { - wrapped_text.push_str(&format!("{line}")); + // TODO: wrapped_text on multiple lines breaks the table layout + wrapped_text.push_str(&format!("{line}\n")); } + wrapped_text = wrapped_text.trim().to_string(); let id = &streak.id.to_string()[0..5]; let index = Style::new().bold().paint(format!("{}", id)); @@ -204,15 +203,13 @@ pub enum SortByDirection { Descending, } -pub fn get_sort_order(sort_by: String) -> Option<(SortByField, SortByDirection)> { +pub fn get_sort_order(sort_by: &str) -> (SortByField, SortByDirection) { + dbg!(&sort_by); let sign = match sort_by.chars().rev().next().unwrap() { - '+' => Some(SortByDirection::Ascending), - '-' => Some(SortByDirection::Descending), - _ => None, + '+' => SortByDirection::Ascending, + '-' => SortByDirection::Descending, + _ => SortByDirection::Ascending }; - if sign.is_none() { - return None; - } let ln = sort_by.len() - 1; let field = match sort_by[..ln].to_lowercase().as_str() { @@ -236,14 +233,14 @@ pub fn get_sort_order(sort_by: String) -> Option<(SortByField, SortByDirection)> _ => SortByField::Task, }; - Some((field, sign.unwrap())) + (field, sign) } /// Parses command line options pub fn parse() { let cli = Cli::parse(); let db_url = get_database_url(); - let mut db = Database::new(db_url.as_str()).expect("Could not load database"); + let mut db = Database::new(&db_url).expect("Could not load database"); let response_style = Style::new().bold().fg(Color::Green); match &cli.command { Commands::Add { task, frequency } => match frequency { @@ -266,16 +263,12 @@ pub fn parse() { }, Commands::List { sort_by, search } => { let mut streak_list = match search.is_empty() { - true => db.get_all().unwrap(), + true => db.get_all(), false => db.search(search), }; - // TODO: change `sort_by` to `&str` - let sort_by = get_sort_order(sort_by.to_string()); + let sort_by = get_sort_order(sort_by); - streak_list = match sort_by { - Some((field, direction)) => sort_streaks(streak_list, field, direction), - None => streak_list, - }; + streak_list = sort_streaks(streak_list, sort_by.0, sort_by.1); println!("{}", build_table(streak_list)); } Commands::Get { ident } => { @@ -395,8 +388,10 @@ mod tests { )) .arg("list") .arg("--sort-by") - .arg(sort_string) + .arg(format!(r#""{}""#, sort_string)) .assert(); list_assert.success(); } + + // TODO: Test search } diff --git a/src/db.rs b/src/db.rs index c860eee..ff81f42 100644 --- a/src/db.rs +++ b/src/db.rs @@ -132,10 +132,10 @@ impl Database { Ok(new_db) } - pub fn get_all(&mut self) -> Option> { + pub fn get_all(&mut self) -> Vec { match self.streaks.len() { - 0 => None, - _ => Some(self.streaks.clone()), + 0 => Vec::::new(), + _ => self.streaks.clone(), } } @@ -288,7 +288,7 @@ mod tests { let streak = Streak::new_daily("brush teeth".to_string()); db.add(streak.clone()).unwrap(); - let result = db.get_all().unwrap(); + let result = db.get_all(); assert_eq!(result.len(), 1); assert_eq!(result[0], streak); @@ -308,7 +308,7 @@ mod tests { streak.task = "floss".to_string(); db.update(streak.id, streak.clone()).unwrap(); - let result = db.get_all().unwrap(); + let result = db.get_all(); assert_eq!(result.len(), 1); assert_eq!(result[0].task, "floss"); @@ -324,12 +324,12 @@ mod tests { let mut db = Database::new(file_path).unwrap(); let streak = Streak::new_daily("brush teeth".to_string()); db.add(streak.clone()).unwrap(); - assert!(db.get_all().is_some()); + assert!(!db.get_all().is_empty()); db.delete(streak.id).unwrap(); let result = db.get_all(); - assert!(result.is_none()); + assert!(result.is_empty()); temp.close().unwrap(); } @@ -346,7 +346,7 @@ mod tests { db.add(streak1.clone()).unwrap(); db.add(streak2.clone()).unwrap(); - let result = db.get_all().unwrap(); + let result = db.get_all(); assert_eq!(result.len(), 2); assert_eq!(result[0], streak1); assert_eq!(result[1], streak2); diff --git a/src/tui.rs b/src/tui.rs index 937a47d..917f993 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -92,7 +92,6 @@ impl App { let mut db = Database::new(&get_database_url()).expect("Failed to load database"); let data_vec: Vec = db .get_all() - .unwrap_or_default() .into_iter() .map(Data::from) .collect(); @@ -118,7 +117,6 @@ impl App { let data_vec: Vec = self .db .get_all() - .unwrap_or_default() .into_iter() .map(Data::from) .collect(); @@ -167,7 +165,7 @@ impl App { pub fn remove(&mut self) { let selected = self.state.selected().unwrap(); - let streak = self.db.get_all().unwrap().get(selected).unwrap().clone(); + let streak = self.db.get_all().get(selected).unwrap().clone(); let _ = self.db.delete(streak.id); let _ = self.db.save(); From 65afb165a6411319b0a2086a25a1468c17bf3a13 Mon Sep 17 00:00:00 2001 From: Kenneth Love <11908+kennethlove@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:17:24 -0700 Subject: [PATCH 4/5] More tests --- src/cli.rs | 110 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 27 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9a78455..28eca55 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,12 +9,12 @@ use dirs; use tabled::{builder::Builder, settings::Style as TabledStyle}; use uuid::Uuid; +use crate::streak::sort_streaks; use crate::{ db::Database, streak::{Frequency, Streak}, tui, }; -use crate::streak::sort_streaks; #[derive(Debug, Parser)] #[command(version, about, long_about = None)] @@ -187,7 +187,7 @@ pub fn get_database_url() -> String { path.to_string_lossy().to_string() } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum SortByField { Task, Frequency, @@ -197,18 +197,17 @@ pub enum SortByField { TotalCheckins, } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum SortByDirection { Ascending, Descending, } pub fn get_sort_order(sort_by: &str) -> (SortByField, SortByDirection) { - dbg!(&sort_by); - let sign = match sort_by.chars().rev().next().unwrap() { - '+' => SortByDirection::Ascending, - '-' => SortByDirection::Descending, - _ => SortByDirection::Ascending + let sign = match sort_by.chars().rev().next() { + Some('+') => SortByDirection::Ascending, + Some('-') => SortByDirection::Descending, + _ => SortByDirection::Ascending, }; let ln = sort_by.len() - 1; @@ -304,6 +303,7 @@ pub fn parse() { #[cfg(test)] mod tests { + use super::{get_sort_order, Streak}; use assert_cmd::Command; use assert_fs::TempDir; use rstest::*; @@ -313,40 +313,49 @@ mod tests { Command::cargo_bin("skidmarks").unwrap() } + #[fixture] + fn streak() -> Streak { + Streak::new_daily("Test Streak".to_string()) + } + #[rstest] fn get_all(mut command: Command) { let temp = TempDir::new().unwrap(); - let list_assert = command + command .arg("--database-url") - .arg(format!("{}{}", temp.path().display(), "test-get-all.ron")) + .arg(format!("{}/{}", temp.path().display(), "test-get-all.ron")) .arg("list") - .assert(); - list_assert.success(); + .assert() + .success(); } #[rstest] fn new_daily_command(mut command: Command) { let temp = TempDir::new().unwrap(); - let add_assert = command + command .arg("--database-url") - .arg(format!("{}{}", temp.path().display(), "test-new-daily.ron")) + .arg(format!( + "{}/{}", + temp.path().display(), + "test-new-daily.ron" + )) .arg("add") .arg("--task") .arg("Test Streak") .arg("--frequency") .arg("daily") - .assert(); - add_assert.success(); + .assert() + .success(); } #[rstest] fn new_weekly_command(mut command: Command) { let temp = TempDir::new().unwrap(); - let add_assert = command + command .arg("--database-url") .arg(format!( - "{}{}", + "{}/{}", temp.path().display(), "test-new-weekly.ron" )) @@ -355,8 +364,8 @@ mod tests { .arg("Test Streak") .arg("--frequency") .arg("weekly") - .assert(); - add_assert.success(); + .assert() + .success(); } #[rstest] @@ -379,19 +388,66 @@ mod tests { mut command: Command, ) { let temp = TempDir::new().unwrap(); - let list_assert = command + let mut list_assert = command .arg("--database-url") .arg(format!( - "{}{}", + "{}/{}", temp.path().display(), "test-sort-order.ron" )) .arg("list") - .arg("--sort-by") - .arg(format!(r#""{}""#, sort_string)) - .assert(); - list_assert.success(); + .arg("--sort-by"); + + #[cfg(target_os = "windows")] + { + list_assert = list_assert.arg(format!(r#""{}""#, sort_string)); + } + #[cfg(not(target_os = "windows"))] + { + list_assert = list_assert.arg(format!("{}", sort_string)); + } + list_assert.assert().success(); } - // TODO: Test search + #[test] + fn test_single_sort_order() { + let sort = "task+"; + let (field, direction) = get_sort_order(sort); + assert_eq!(field, super::SortByField::Task); + assert_eq!(direction, super::SortByDirection::Ascending); + } + + #[rstest] + fn test_search(mut command: Command) { + let temp = TempDir::new().unwrap(); + + command + .arg("--database-url") + .arg(format!("{}/{}", temp.path().display(), "test-search.ron")) + .arg("list") + .arg("--search") + .arg("Test") + .assert() + .success(); + } + + #[rstest] + fn test_search_and_sort(mut command: Command) { + let temp = TempDir::new().unwrap(); + + command + .arg("--database-url") + .arg(format!( + "{}/{}", + temp.path().display(), + "test-search-sort.ron" + )) + .arg("list") + .arg("--search") + .arg("Test") + .arg("--sort-by") + .arg("task+") + .assert() + .success(); + } } From b8857a49a51f1c04f39255438ebe4d1888bb6e0f Mon Sep 17 00:00:00 2001 From: Kenneth Love <11908+kennethlove@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:10:16 -0700 Subject: [PATCH 5/5] multiline text should look better/good --- Cargo.lock | 11 +++++++++++ Cargo.toml | 3 ++- src/cli.rs | 15 ++++++--------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18c279e..090ae55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1894,6 +1894,7 @@ dependencies = [ "serde", "serde_json", "tabled", + "term_size", "textwrap", "tui-input", "tui_confirm_dialog", @@ -2075,6 +2076,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "term_size" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "termtree" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 97aa465..065e8a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ ron = "0.8.1" serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.122" tabled = { version = "0.15.0", features = ["ansi"] } +term_size = "0.3.2" textwrap = { version = "0.16.1" } tui-input = "0.9.0" tui_confirm_dialog = "0.2.2" @@ -35,4 +36,4 @@ unicode-width = "0.1.13" uuid = { version = "1.10.0", features = ["serde", "v4", "fast-rng"] } [dev-dependencies] -rstest = "0.22.0" \ No newline at end of file +rstest = "0.22.0" diff --git a/src/cli.rs b/src/cli.rs index 28eca55..d93ab2d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,12 +1,12 @@ use std::path::Path; use ansi_term::{Color, Style}; -#[allow(unused_imports)] -use chrono::{Local, NaiveDate}; +use chrono::Local; use clap::{Parser, Subcommand}; use console::Emoji; use dirs; use tabled::{builder::Builder, settings::Style as TabledStyle}; +use term_size::dimensions; use uuid::Uuid; use crate::streak::sort_streaks; @@ -137,9 +137,11 @@ fn build_table(streaks: Vec) -> String { header_style.paint("\nTotal").to_string(), ]); + let (width, _) = dimensions().unwrap(); + for streak in streaks.iter() { let mut wrapped_text = String::new(); - let wrapped_lines = textwrap::wrap(&streak.task.as_str(), 40); + let wrapped_lines = textwrap::wrap(&streak.task.as_str(), width - 90); for line in wrapped_lines { // TODO: wrapped_text on multiple lines breaks the table layout wrapped_text.push_str(&format!("{line}\n")); @@ -149,7 +151,7 @@ fn build_table(streaks: Vec) -> String { let id = &streak.id.to_string()[0..5]; let index = Style::new().bold().paint(format!("{}", id)); let streak_name = Style::new().bold().paint(wrapped_text); - let frequency = Style::new().paint(format!("{}", &streak.frequency)); + let frequency = Style::new().paint(format!("{:^6}", &streak.frequency)); let emoji = Style::new().paint(format!("{:^6}", &streak.emoji_status())); let check_in = match &streak.last_checkin { Some(date) => date.to_string(), @@ -313,11 +315,6 @@ mod tests { Command::cargo_bin("skidmarks").unwrap() } - #[fixture] - fn streak() -> Streak { - Streak::new_daily("Test Streak".to_string()) - } - #[rstest] fn get_all(mut command: Command) { let temp = TempDir::new().unwrap();