Skip to content

Commit

Permalink
now empy todos are skipped by default
Browse files Browse the repository at this point in the history
  • Loading branch information
VladimirMarkelov committed Jan 29, 2023
1 parent 1c7443e commit d83e0e2
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 42 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "todo_lib"
version = "6.0.1"
version = "6.1.0"
authors = ["Vladimir Markelov <vmatroskin@gmail.com>"]
edition = "2018"
keywords = ["todotxt"]
Expand Down
3 changes: 3 additions & 0 deletions changelog
@@ -1,3 +1,6 @@
2023-01-29 - version 6.1.0
[+] Empty todos are skipped by default. To show them, use `--all` flag

2022-12-21 - version 6.0.0
[+] Add API to edit any tag in the subject. Standard tags are ignored
by the new API, use the existing specific functions to change due
Expand Down
22 changes: 20 additions & 2 deletions src/tfilter.rs
Expand Up @@ -27,14 +27,16 @@ pub enum ItemRange {
}

/// Todo state range
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum TodoStatus {
/// Only todos that are incompleted yet
Active,
/// All todos
All,
/// Only todos marked `done`
Done,
/// Only empty todos
Empty,
}

/// An arbitrary range of values for todo properties check. The range is inclusive
Expand Down Expand Up @@ -226,7 +228,7 @@ fn filter_regex(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVe

let mut new_v: todo::IDVec = Vec::new();
if c.use_regex {
let rx = match Regex::new(&format!("(?i){}", rx)) {
let rx = match Regex::new(&format!("(?i){rx}")) {
Err(_) => {
println!("Invalid regex");
return v;
Expand Down Expand Up @@ -258,6 +260,21 @@ fn filter_regex(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVe
new_v
}

fn filter_empty(tasks: &todo::TaskSlice, v: todo::IDVec, c: &Conf) -> todo::IDVec {
if c.all == TodoStatus::All {
return v;
}
let mut new_v: todo::IDVec = Vec::new();
for i in v.iter() {
let idx = *i;
let empty = tasks[idx].subject.is_empty();
if (empty && c.all == TodoStatus::Empty) || (!empty && c.all != TodoStatus::Empty) {
new_v.push(idx);
}
}
new_v
}

fn vec_match(task_list: &[String], filter: &[String]) -> bool {
if filter.is_empty() {
return true;
Expand Down Expand Up @@ -603,6 +620,7 @@ pub fn filter(tasks: &todo::TaskSlice, c: &Conf) -> todo::IDVec {
}
}
}
v = filter_empty(tasks, v, c);
v = filter_regex(tasks, v, c);
v = filter_tag(tasks, v, c);
v = filter_hashtag(tasks, v, c);
Expand Down
2 changes: 1 addition & 1 deletion src/timer.rs
Expand Up @@ -71,7 +71,7 @@ pub fn stop_timer(task: &mut todotxt::Task) -> bool {
}

if let Some(spent) = calc_time_spent(task) {
let new_spent = format!("{}", spent);
let new_spent = format!("{spent}");
task.update_tag_with_value(todo::SPENT_TAG, &new_spent);
task.update_tag_with_value(todo::TIMER_TAG, todo::TIMER_OFF);
return true;
Expand Down
16 changes: 8 additions & 8 deletions src/todo.rs
Expand Up @@ -195,8 +195,8 @@ pub fn save(tasks: &TaskSlice, filename: &Path) -> Result<(), terr::TodoError> {

let mut output = File::create(&tmpname).map_err(|_| terr::TodoError::SaveFailed)?;
for t in tasks {
let line = format!("{}\n", t);
write!(output, "{}", line).map_err(|_| terr::TodoError::FileWriteFailed)?;
let line = format!("{t}\n");
write!(output, "{line}").map_err(|_| terr::TodoError::FileWriteFailed)?;
}

fs::rename(tmpname, filename).map_err(|e| terr::TodoError::IOError(e.to_string()))?;
Expand All @@ -218,8 +218,8 @@ pub fn archive(tasks: &TaskSlice, filename: &Path) -> Result<(), terr::TodoError
.map_err(|_| terr::TodoError::AppendFailed)?;

for t in tasks {
let line = format!("{}\n", t);
write!(output, "{}", line).map_err(|_| terr::TodoError::FileWriteFailed)?;
let line = format!("{t}\n");
write!(output, "{line}").map_err(|_| terr::TodoError::FileWriteFailed)?;
}

Ok(())
Expand Down Expand Up @@ -470,7 +470,7 @@ fn update_recurrence(task: &mut todotxt::Task, c: &Conf) -> bool {
Action::Set => {
if !tsort::equal_opt_rec(&task.recurrence, &c.recurrence) {
if let Some(nr) = &c.recurrence {
let new_rec = format!("{}", nr);
let new_rec = format!("{nr}");
let updated = task.update_tag(&new_rec);
if updated && task.finished {
task.uncomplete();
Expand Down Expand Up @@ -588,7 +588,7 @@ fn hashtag_update_check(task: &mut todotxt::Task, hashtag: &str, act: Action) ->
let old_subj = task.subject.clone();
let mut new_subj = old_subj.clone();
let hashtag = hashtag.trim_start_matches('#');
let hashtag_full = format!("#{}", hashtag);
let hashtag_full = format!("#{hashtag}");
match act {
Action::Delete => {
task.hashtags.retain(|h| h != hashtag);
Expand All @@ -613,8 +613,8 @@ fn hashtag_update_check(task: &mut todotxt::Task, hashtag: &str, act: Action) ->
if old != new && task.hashtags.contains(&old.to_string()) {
task.hashtags.retain(|h| h != &old_str);
task.hashtags.push(new.to_string());
let old = format!("#{}", old);
let new = format!("#{}", new);
let old = format!("#{old}");
let new = format!("#{new}");
todotxt::replace_word(&mut new_subj, &old, &new);
task.subject = new_subj;
return true;
Expand Down
24 changes: 12 additions & 12 deletions src/todotxt/task.rs
Expand Up @@ -101,8 +101,8 @@ impl Task {
if name == "t" {
if let Ok(dt) = utils::parse_date(value, base) {
self.threshold_date = Some(dt);
let old_tag = format!("{}:{}", name, value);
let new_tag = format!("{}:{}", name, utils::format_date(dt));
let old_tag = format!("{name}:{value}");
let new_tag = format!("{name}:{0}", utils::format_date(dt));
if old_tag != new_tag {
old_tags.push(old_tag);
new_tags.push(new_tag);
Expand All @@ -112,8 +112,8 @@ impl Task {
if name == "due" {
if let Ok(dt) = utils::parse_date(value, base) {
self.due_date = Some(dt);
let old_tag = format!("{}:{}", name, value);
let new_tag = format!("{}:{}", name, utils::format_date(dt));
let old_tag = format!("{name}:{value}");
let new_tag = format!("{name}:{0}", utils::format_date(dt));
if old_tag != new_tag {
old_tags.push(old_tag);
new_tags.push(new_tag);
Expand Down Expand Up @@ -231,7 +231,7 @@ impl Task {
if value.is_empty() {
let old = self.tags.remove(tag);
if let Some(v) = old {
let old_tag = format!("{}:{}", tag, v);
let old_tag = format!("{tag}:{v}");
self.replace_tag(&old_tag, value);
self.update_field(tag, value);
return true;
Expand All @@ -241,15 +241,15 @@ impl Task {
#[allow(clippy::format_push_string)]
match self.tags.get(tag) {
None => {
self.subject += &format!(" {}:{}", tag, value);
self.subject += &format!(" {tag}:{value}");
self.tags.insert(tag.to_string(), value.to_string());
self.update_field(tag, value);
true
}
Some(v) => {
if v != value {
let old_tag = format!("{}:{}", tag, v);
let new_tag = format!("{}:{}", tag, value);
let old_tag = format!("{tag}:{v}");
let new_tag = format!("{tag}:{value}");
self.replace_tag(&old_tag, &new_tag);
self.update_field(tag, value);
true
Expand Down Expand Up @@ -375,9 +375,9 @@ impl Task {
self.projects.retain(|proj| proj != old);
if !new.is_empty() {
self.projects.push(new.to_string());
utils::replace_word(&mut self.subject, &format!("+{}", old), &format!("+{}", new));
utils::replace_word(&mut self.subject, &format!("+{old}"), &format!("+{new}"));
} else {
utils::replace_word(&mut self.subject, &format!("+{}", old), "");
utils::replace_word(&mut self.subject, &format!("+{old}"), "");
}
}

Expand All @@ -398,9 +398,9 @@ impl Task {
self.contexts.retain(|proj| proj != old);
if !new.is_empty() {
self.contexts.push(new.to_string());
utils::replace_word(&mut self.subject, &format!("@{}", old), &format!("@{}", new));
utils::replace_word(&mut self.subject, &format!("@{old}"), &format!("@{new}"));
} else {
utils::replace_word(&mut self.subject, &format!("@{}", old), "");
utils::replace_word(&mut self.subject, &format!("@{old}"), "");
}
}
}
36 changes: 18 additions & 18 deletions src/todotxt/utils.rs
Expand Up @@ -67,15 +67,15 @@ pub fn split_tag(s: &str) -> Option<(&str, &str)> {
/// A string must be a capital Latin letter enclosed in parentheses.
pub fn parse_priority(s: &str) -> Result<u8, String> {
if s.len() != 3 {
return Err(format!("invalid priority '{}'", s));
return Err(format!("invalid priority '{s}'"));
}
let trimmed = s.trim_matches(|c| c == ' ' || c == '(' || c == ')');
if trimmed.len() != 1 {
return Err(format!("invalid priority '{}'", s));
return Err(format!("invalid priority '{s}'"));
}
let priority = trimmed.bytes().next().expect("impossible");
if !(b'A'..=b'Z').contains(&priority) {
return Err(format!("invalid priority '{}'", s));
return Err(format!("invalid priority '{s}'"));
}
Ok(priority - b'A')
}
Expand All @@ -97,29 +97,29 @@ pub fn parse_date(s: &str, base: NaiveDate) -> Result<NaiveDate, String> {

if s.find('-').is_none() {
match s.parse::<Recurrence>() {
Err(_) => return Err(format!("invalid date '{}'", s)),
Err(_) => return Err(format!("invalid date '{s}'")),
Ok(rec) => return Ok(rec.next_date(base)),
}
}

let mut vals: Vec<u32> = Vec::new();
for spl in trimmed.split('-') {
match spl.parse::<u32>() {
Err(_) => return Err(format!("invalid date '{}'", s)),
Err(_) => return Err(format!("invalid date '{s}'")),
Ok(n) => vals.push(n),
}
}
if vals.len() != 3 {
return Err(format!("invalid date '{}'", s));
return Err(format!("invalid date '{s}'"));
}
if vals[0] == 0 {
return Err(format!("invalid year '{}'", s));
return Err(format!("invalid year '{s}'"));
}
if vals[1] == 0 || vals[1] > 12 {
return Err(format!("invalid month '{}'", s));
return Err(format!("invalid month '{s}'"));
}
if vals[2] == 0 || vals[2] > 31 {
return Err(format!("invalid day '{}'", s));
return Err(format!("invalid day '{s}'"));
}
let mx = days_in_month(vals[0] as i32, vals[1]);
if vals[2] > mx {
Expand All @@ -136,11 +136,11 @@ pub fn format_date(date: NaiveDate) -> String {
}

pub fn extract_projects(s: &str) -> Vec<String> {
extract_anything(&format!(" {} ", s), " +")
extract_anything(&format!(" {s} "), " +")
}

pub fn extract_contexts(s: &str) -> Vec<String> {
extract_anything(&format!(" {} ", s), " @")
extract_anything(&format!(" {s} "), " @")
}

fn extract_anything(s: &str, start_from: &str) -> Vec<String> {
Expand Down Expand Up @@ -200,19 +200,19 @@ pub fn replace_word(s: &mut String, old: &str, new: &str) {
s.replace_range(.., new);
return;
}
if s.starts_with(&format!("{} ", old)) {
if s.starts_with(&format!("{old} ")) {
let l = if new.is_empty() { old.len() + 1 } else { old.len() };
println!("replacing {} in {} with {}", l, s, new);
println!("replacing {l} in {s} with {new}");
s.replace_range(..l, new);
}
if s.ends_with(&format!(" {}", old)) {
if s.ends_with(&format!(" {old}")) {
let l = if new.is_empty() { old.len() + 1 } else { old.len() };
s.replace_range(s.len() - l.., new);
}
if new.is_empty() {
*s = s.replace(&format!(" {} ", old), " ");
*s = s.replace(&format!(" {old} "), " ");
} else {
*s = s.replace(&format!(" {} ", old), &format!(" {} ", new));
*s = s.replace(&format!(" {old} "), &format!(" {new} "));
}
}

Expand Down Expand Up @@ -258,14 +258,14 @@ impl Recurrence {
} else if s.ends_with('y') {
rec.period = Period::Year;
} else {
return Err(format!("invalid recurrence '{}'", s));
return Err(format!("invalid recurrence '{s}'"));
}
if s.starts_with('+') {
rec.strict = true;
}
let num = s[..s.len() - 1].parse::<u8>();
match num {
Err(_) => Err(format!("invalid recurrence '{}'", s)),
Err(_) => Err(format!("invalid recurrence '{s}'")),
Ok(n) => {
rec.count = n;
Ok(rec)
Expand Down
32 changes: 32 additions & 0 deletions tests/flt.rs
@@ -1,4 +1,5 @@
use chrono;
use todo_lib::tfilter::TodoStatus;
use todo_lib::{tfilter, todo, todotxt, tsort};

fn init_tasks() -> todo::TaskVec {
Expand All @@ -21,6 +22,17 @@ fn init_tasks() -> todo::TaskVec {
t
}

fn init_tasks_with_empty() -> todo::TaskVec {
let mut t = Vec::new();
let now = chrono::Local::now().date_naive();

t.push(todotxt::Task::parse("call mother #tagone +family @parents", now));
t.push(todotxt::Task::parse("", now));
t.push(todotxt::Task::parse("xmas vacations +FamilyHoliday due:2018-12-24", now));
t.push(todotxt::Task::parse("", now));
t
}

#[test]
fn one_item() {
let t = init_tasks();
Expand Down Expand Up @@ -298,3 +310,23 @@ fn item_tags() {
assert_eq!(ids, test.res, "{}. {:?} != {:?}", idx, ids, test.res);
}
}

#[test]
fn empty_tasks() {
let t = init_tasks_with_empty();
struct Test {
inc: TodoStatus,
res: todo::IDVec,
}
let tests: Vec<Test> = vec![
Test { inc: TodoStatus::Active, res: vec![0, 2] },
Test { inc: TodoStatus::Empty, res: vec![1, 3] },
Test { inc: TodoStatus::All, res: vec![0, 1, 2, 3] },
];
for (idx, test) in tests.iter().enumerate() {
let mut cflt = tfilter::Conf::default();
cflt.all = test.inc;
let ids = tfilter::filter(&t, &cflt);
assert_eq!(ids, test.res, "{idx}. {ids:?} != {:?}", test.res);
}
}

0 comments on commit d83e0e2

Please sign in to comment.