A file-based task manager

chore: Make clippy happy

Fix all clippy lints, except one, I kept it for @ngp because I'm not
100% sure what the correct behavior is

```rust
// src/util.rs:11
pub fn flopen(path: PathBuf, mode: FlockArg) -> Result<Flock<File>> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true) // clippy: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_open_options
.open(path)?;
Flock::lock(file, mode).map_err(|(_, errno)| Error::Lock(errno))
}
```

Fixes: #4
Signed-off-by: Awiteb <a@4rs.nl>

authored by Awiteb and committed by ngp.computer 48441e03 77106873

+62 -69
+9 -7
src/attrs.rs
··· 1 + use std::collections::btree_map::Entry; 1 2 use std::collections::btree_map::{IntoIter as BTreeIntoIter, Iter as BTreeMapIter}; 2 3 use std::collections::BTreeMap; 3 4 use std::iter::Chain; ··· 20 21 type IntoIter = Chain<BTreeIntoIter<String, String>, BTreeIntoIter<String, String>>; 21 22 22 23 fn into_iter(self) -> Self::IntoIter { 23 - self.written.into_iter().chain(self.updated.into_iter()) 24 + self.written.into_iter().chain(self.updated) 24 25 } 25 26 } 26 27 ··· 38 39 } 39 40 40 41 pub(crate) fn insert(&mut self, key: String, value: String) -> Option<String> { 41 - if self.updated.contains_key(&key) { 42 - self.updated.insert(key, value) 43 - } else { 44 - let maybe_old_value = self.written.get(&key); 45 - self.updated.insert(key, value); 46 - maybe_old_value.cloned() 42 + match self.updated.entry(key.clone()) { 43 + Entry::Occupied(mut e) => Some(e.insert(value)), 44 + Entry::Vacant(e) => { 45 + e.insert(value); 46 + let maybe_old_value = self.written.get(&key); 47 + maybe_old_value.cloned() 48 + } 47 49 } 48 50 } 49 51
+1 -1
src/fzf.rs
··· 20 20 // unwrap: this can never fail 21 21 let child_in = child.stdin.as_mut().unwrap(); 22 22 for item in input.into_iter() { 23 - write!(child_in, "{item}\n")?; 23 + writeln!(child_in, "{item}")?; 24 24 } 25 25 let output = child.wait_with_output()?; 26 26 if output.stdout.is_empty() {
+20 -32
src/main.rs
··· 25 25 } 26 26 27 27 fn parse_id(s: &str) -> std::result::Result<Id, &'static str> { 28 - Ok(Id::from_str(s).map_err(|_| "Unable to parse tsk- ID")?) 28 + Id::from_str(s).map_err(|_| "Unable to parse tsk- ID") 29 29 } 30 30 31 31 #[derive(Parser)] ··· 235 235 fn from(value: TaskId) -> Self { 236 236 if let Some(id) = value.id.map(Id::from).or(value.tsk_id) { 237 237 TaskIdentifier::Id(id) 238 - } else { 239 - if value.find.find { 240 - TaskIdentifier::Find { 241 - search_body: value.find.args.search_body, 242 - archived: false, 243 - } 244 - } else { 245 - TaskIdentifier::Relative(value.relative_id) 238 + } else if value.find.find { 239 + TaskIdentifier::Find { 240 + search_body: value.find.args.search_body, 241 + archived: false, 246 242 } 243 + } else { 244 + TaskIdentifier::Relative(value.relative_id) 247 245 } 248 246 } 249 247 } ··· 311 309 let mut title = if let Some(title) = title.title { 312 310 title 313 311 } else if let Some(title) = title.title_simple { 314 - let joined = title.join(" "); 315 - joined 312 + title.join(" ") 316 313 } else { 317 314 "".to_string() 318 315 }; ··· 349 346 350 347 fn command_list(dir: PathBuf, all: bool, count: usize) -> Result<()> { 351 348 let workspace = Workspace::from_path(dir)?; 352 - let stack = if all { 353 - workspace.read_stack()? 354 - } else { 355 - workspace.read_stack()? 356 - }; 349 + let stack = workspace.read_stack()?; 350 + 357 351 if stack.empty() { 358 352 println!("*No tasks*"); 359 353 exit(0); 360 - } else { 361 - if !all { 362 - for stack_item in stack.into_iter().take(count) { 363 - if let Some(parsed) = task::parse(&stack_item.title) { 364 - println!("{}\t{}", stack_item.id, parsed.content.trim()); 365 - } else { 366 - println!("{stack_item}"); 367 - } 368 - } 354 + } 355 + 356 + for (_, stack_item) in stack 357 + .into_iter() 358 + .enumerate() 359 + .take_while(|(idx, _)| all || idx < &count) 360 + { 361 + if let Some(parsed) = task::parse(&stack_item.title) { 362 + println!("{}\t{}", stack_item.id, parsed.content.trim()); 369 363 } else { 370 - for stack_item in stack.into_iter() { 371 - if let Some(parsed) = task::parse(&stack_item.title) { 372 - println!("{}\t{}", stack_item.id, parsed.content.trim()); 373 - } else { 374 - println!("{stack_item}"); 375 - } 376 - } 364 + println!("{stack_item}"); 377 365 } 378 366 } 379 367 Ok(())
+15 -14
src/stack.rs
··· 7 7 use std::collections::vec_deque::Iter; 8 8 use std::collections::VecDeque; 9 9 use std::fmt::Display; 10 + use std::fs::File; 10 11 use std::io::{self, BufRead, BufReader, Seek, Write}; 12 + use std::path::Path; 11 13 use std::str::FromStr; 12 14 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 13 - use std::{fs::File, path::PathBuf}; 14 15 15 16 use nix::fcntl::{Flock, FlockArg}; 16 17 ··· 67 68 let mut parts = s.trim().split("\t"); 68 69 let id: Id = parts 69 70 .next() 70 - .ok_or(Error::Parse(format!( 71 - "Incomplete index line. Missing tsk ID" 72 - )))? 71 + .ok_or(Error::Parse( 72 + "Incomplete index line. Missing tsk ID".to_owned(), 73 + ))? 73 74 .parse()?; 74 75 let title: String = parts 75 76 .next() 76 - .ok_or(Error::Parse(format!( 77 - "Incomplete index line. Missing title." 78 - )))? 77 + .ok_or(Error::Parse( 78 + "Incomplete index line. Missing title.".to_owned(), 79 + ))? 79 80 .trim() 80 81 .to_string(); 81 82 // parse the timestamp as an integer ··· 96 97 97 98 impl StackItem { 98 99 /// Parses a [`StackItem`] from a string. The expected format is a tab-delimited line with the 99 - /// files: task id title 100 - fn from_line(workspace_path: &PathBuf, line: String) -> Result<Self> { 100 + /// files: task id title 101 + fn from_line(workspace_path: &Path, line: String) -> Result<Self> { 101 102 let mut stack_item: StackItem = line.parse()?; 102 103 103 104 let task = util::flopen( 104 105 workspace_path 105 106 .join(TASKSFOLDER) 106 - .join(stack_item.id.to_filename()), 107 + .join(stack_item.id.filename()), 107 108 FlockArg::LockExclusive, 108 109 )?; 109 110 let task_modify_time = task.metadata()?.modified()?; ··· 125 126 } 126 127 127 128 impl TaskStack { 128 - pub fn from_tskdir(workspace_path: &PathBuf) -> Result<Self> { 129 + pub fn from_tskdir(workspace_path: &Path) -> Result<Self> { 129 130 let file = util::flopen(workspace_path.join(INDEXFILE), FlockArg::LockExclusive)?; 130 131 let index = BufReader::new(&*file).lines(); 131 132 let mut all = VecDeque::new(); ··· 164 165 pub fn swap(&mut self) { 165 166 let tip = self.all.pop_front(); 166 167 let second = self.all.pop_front(); 167 - if tip.is_some() && second.is_some() { 168 - self.all.push_front(tip.unwrap()); 169 - self.all.push_front(second.unwrap()); 168 + if let Some((tip, second)) = tip.zip(second) { 169 + self.all.push_front(tip); 170 + self.all.push_front(second); 170 171 } 171 172 } 172 173
+5 -5
src/task.rs
··· 86 86 (']', ']', Some(InternalLink(il, s_pos))) => { 87 87 state.pop(); 88 88 let contents = s.get(s_pos + 1..char_pos - 1)?; 89 - if let Ok(id) = Id::from_str(&contents) { 89 + if let Ok(id) = Id::from_str(contents) { 90 90 let linktext = format!( 91 91 "{}{}", 92 92 contents.purple(), ··· 291 291 fn test_link_no_terminal_link() { 292 292 let input = "hello [world](https://ngp.computer\n"; 293 293 let output = parse(input).expect("parse to work"); 294 - assert!(output.links.len() == 0); 294 + assert!(output.links.is_empty()); 295 295 assert_eq!(input, output.content); 296 296 } 297 297 #[test] 298 298 fn test_link_bad_no_start_link() { 299 299 let input = "hello [world]https://ngp.computer)\n"; 300 300 let output = parse(input).expect("parse to work"); 301 - assert!(output.links.len() == 0); 301 + assert!(output.links.is_empty()); 302 302 assert_eq!(input, output.content); 303 303 } 304 304 #[test] 305 305 fn test_link_bad_no_link() { 306 306 let input = "hello [world]\n"; 307 307 let output = parse(input).expect("parse to work"); 308 - assert!(output.links.len() == 0); 308 + assert!(output.links.is_empty()); 309 309 assert_eq!(input, output.content); 310 310 } 311 311 ··· 324 324 fn test_internal_link_bad() { 325 325 let input = "hello [[tsk-123"; 326 326 let output = parse(input).expect("parse to work"); 327 - assert!(output.links.len() == 0); 327 + assert!(output.links.is_empty()); 328 328 assert_eq!(input, output.content); 329 329 } 330 330
+1 -1
src/util.rs
··· 14 14 .write(true) 15 15 .create(true) 16 16 .open(path)?; 17 - Ok(Flock::lock(file, mode).map_err(|(_, errno)| Error::Lock(errno))?) 17 + Flock::lock(file, mode).map_err(|(_, errno)| Error::Lock(errno)) 18 18 } 19 19 20 20 /// Recursively searches upwards for a directory
+11 -9
src/workspace.rs
··· 51 51 } 52 52 53 53 impl Id { 54 - pub fn to_filename(&self) -> String { 54 + /// Returns the filename for a task with this id. 55 + pub fn filename(&self) -> String { 55 56 format!("tsk-{}.tsk", self.0) 56 57 } 57 58 } ··· 83 84 } 84 85 std::fs::create_dir(&tsk_dir)?; 85 86 // Create the tasks directory 86 - std::fs::create_dir(&tsk_dir.join("tasks"))?; 87 + std::fs::create_dir(tsk_dir.join("tasks"))?; 87 88 // Create the archive directory 88 - std::fs::create_dir(&tsk_dir.join("archive"))?; 89 + std::fs::create_dir(tsk_dir.join("archive"))?; 89 90 let mut next = OpenOptions::new() 90 91 .read(true) 91 92 .write(true) 92 93 .create(true) 94 + .truncate(true) 93 95 .open(tsk_dir.join("next"))?; 94 96 next.write_all(b"1\n")?; 95 97 Ok(()) ··· 212 214 backlinks.insert(from); 213 215 Self::set_xattr( 214 216 &to_task.file, 215 - BACKREFXATTR.into(), 217 + BACKREFXATTR, 216 218 &itertools::join(backlinks, ","), 217 219 ) 218 220 } ··· 228 230 backlinks.remove(&from); 229 231 Self::set_xattr( 230 232 &to_task.file, 231 - BACKREFXATTR.into(), 233 + BACKREFXATTR, 232 234 &itertools::join(backlinks, ","), 233 235 ) 234 236 } ··· 424 426 type Err = Error; 425 427 426 428 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 427 - let (tsk_id, task_content) = s.split_once('\t').ok_or(Error::Parse(format!( 428 - "Missing TSK-ID or content or task parse." 429 - )))?; 429 + let (tsk_id, task_content) = s.split_once('\t').ok_or(Error::Parse( 430 + "Missing TSK-ID or content or task parse.".to_owned(), 431 + ))?; 430 432 let (title, body) = task_content 431 433 .split_once('\t') 432 - .ok_or(Error::Parse(format!("Missing body for task parse.")))?; 434 + .ok_or(Error::Parse("Missing body for task parse.".to_owned()))?; 433 435 Ok(Self { 434 436 id: tsk_id.parse()?, 435 437 title: title.to_string(),