A file-based task manager

ADD: correct full body search flag to find

+119 -36
+22 -31
Cargo.lock
··· 406 407 [[package]] 408 name = "futures" 409 - version = "0.3.30" 410 source = "registry+https://github.com/rust-lang/crates.io-index" 411 - checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 412 dependencies = [ 413 "futures-channel", 414 "futures-core", ··· 421 422 [[package]] 423 name = "futures-channel" 424 - version = "0.3.30" 425 source = "registry+https://github.com/rust-lang/crates.io-index" 426 - checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 427 dependencies = [ 428 "futures-core", 429 "futures-sink", ··· 431 432 [[package]] 433 name = "futures-core" 434 - version = "0.3.30" 435 source = "registry+https://github.com/rust-lang/crates.io-index" 436 - checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 437 438 [[package]] 439 name = "futures-executor" 440 - version = "0.3.30" 441 source = "registry+https://github.com/rust-lang/crates.io-index" 442 - checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 443 dependencies = [ 444 "futures-core", 445 "futures-task", ··· 448 449 [[package]] 450 name = "futures-io" 451 - version = "0.3.30" 452 source = "registry+https://github.com/rust-lang/crates.io-index" 453 - checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 454 455 [[package]] 456 name = "futures-lite" ··· 467 468 [[package]] 469 name = "futures-macro" 470 - version = "0.3.30" 471 source = "registry+https://github.com/rust-lang/crates.io-index" 472 - checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 473 dependencies = [ 474 "proc-macro2", 475 "quote", ··· 478 479 [[package]] 480 name = "futures-sink" 481 - version = "0.3.30" 482 source = "registry+https://github.com/rust-lang/crates.io-index" 483 - checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 484 485 [[package]] 486 name = "futures-task" 487 - version = "0.3.30" 488 source = "registry+https://github.com/rust-lang/crates.io-index" 489 - checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 490 491 [[package]] 492 name = "futures-util" 493 - version = "0.3.30" 494 source = "registry+https://github.com/rust-lang/crates.io-index" 495 - checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 496 dependencies = [ 497 "futures-channel", 498 "futures-core", ··· 590 591 [[package]] 592 name = "iocraft-macros" 593 - version = "0.1.5" 594 source = "registry+https://github.com/rust-lang/crates.io-index" 595 - checksum = "b2737d46d5f3c13db67066e5055ccda0d2379f993f77ce2ca90cf1dbd92edfe4" 596 dependencies = [ 597 "proc-macro2", 598 "quote", ··· 682 683 [[package]] 684 name = "once_cell" 685 - version = "1.20.1" 686 source = "registry+https://github.com/rust-lang/crates.io-index" 687 - checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" 688 - dependencies = [ 689 - "portable-atomic", 690 - ] 691 692 [[package]] 693 name = "parking" ··· 755 "tracing", 756 "windows-sys 0.59.0", 757 ] 758 - 759 - [[package]] 760 - name = "portable-atomic" 761 - version = "1.9.0" 762 - source = "registry+https://github.com/rust-lang/crates.io-index" 763 - checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" 764 765 [[package]] 766 name = "proc-macro2"
··· 406 407 [[package]] 408 name = "futures" 409 + version = "0.3.31" 410 source = "registry+https://github.com/rust-lang/crates.io-index" 411 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 412 dependencies = [ 413 "futures-channel", 414 "futures-core", ··· 421 422 [[package]] 423 name = "futures-channel" 424 + version = "0.3.31" 425 source = "registry+https://github.com/rust-lang/crates.io-index" 426 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 427 dependencies = [ 428 "futures-core", 429 "futures-sink", ··· 431 432 [[package]] 433 name = "futures-core" 434 + version = "0.3.31" 435 source = "registry+https://github.com/rust-lang/crates.io-index" 436 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 437 438 [[package]] 439 name = "futures-executor" 440 + version = "0.3.31" 441 source = "registry+https://github.com/rust-lang/crates.io-index" 442 + checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 443 dependencies = [ 444 "futures-core", 445 "futures-task", ··· 448 449 [[package]] 450 name = "futures-io" 451 + version = "0.3.31" 452 source = "registry+https://github.com/rust-lang/crates.io-index" 453 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 454 455 [[package]] 456 name = "futures-lite" ··· 467 468 [[package]] 469 name = "futures-macro" 470 + version = "0.3.31" 471 source = "registry+https://github.com/rust-lang/crates.io-index" 472 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 473 dependencies = [ 474 "proc-macro2", 475 "quote", ··· 478 479 [[package]] 480 name = "futures-sink" 481 + version = "0.3.31" 482 source = "registry+https://github.com/rust-lang/crates.io-index" 483 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 484 485 [[package]] 486 name = "futures-task" 487 + version = "0.3.31" 488 source = "registry+https://github.com/rust-lang/crates.io-index" 489 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 490 491 [[package]] 492 name = "futures-util" 493 + version = "0.3.31" 494 source = "registry+https://github.com/rust-lang/crates.io-index" 495 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 496 dependencies = [ 497 "futures-channel", 498 "futures-core", ··· 590 591 [[package]] 592 name = "iocraft-macros" 593 + version = "0.1.6" 594 source = "registry+https://github.com/rust-lang/crates.io-index" 595 + checksum = "1c23693fa666552feadf75efc3e0da9d144f3bea30b6a703d3f0b8adf8619434" 596 dependencies = [ 597 "proc-macro2", 598 "quote", ··· 682 683 [[package]] 684 name = "once_cell" 685 + version = "1.20.2" 686 source = "registry+https://github.com/rust-lang/crates.io-index" 687 + checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 688 689 [[package]] 690 name = "parking" ··· 752 "tracing", 753 "windows-sys 0.59.0", 754 ] 755 756 [[package]] 757 name = "proc-macro2"
+1 -1
src/fzf.rs
··· 20 // unwrap: this can never fail 21 let child_in = child.stdin.as_mut().unwrap(); 22 for item in input.into_iter() { 23 - write!(child_in, "{}\n", item.to_string())?; 24 } 25 let output = child.wait_with_output()?; 26 if output.stdout.is_empty() {
··· 20 // unwrap: this can never fail 21 let child_in = child.stdin.as_mut().unwrap(); 22 for item in input.into_iter() { 23 + write!(child_in, "{item}\n")?; 24 } 25 let output = child.wait_with_output()?; 26 if output.stdout.is_empty() {
+1 -1
src/main.rs
··· 292 println!("{}", id.0); 293 } 294 } else { 295 - eprintln!("No task to drop."); 296 exit(1); 297 } 298 Ok(())
··· 292 println!("{}", id.0); 293 } 294 } else { 295 + eprintln!("No task selected."); 296 exit(1); 297 } 298 Ok(())
+95 -3
src/workspace.rs
··· 2 use nix::fcntl::{Flock, FlockArg}; 3 4 use crate::errors::{Error, Result}; 5 - use crate::stack::TaskStack; 6 use crate::{fzf, util}; 7 use std::fmt::Display; 8 use std::fs::{self, File}; 9 use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; ··· 229 pub fn search( 230 &self, 231 stack: Option<TaskStack>, 232 - _search_body: bool, 233 _include_archived: bool, 234 ) -> Result<Option<Id>> { 235 let stack = if let Some(stack) = stack { ··· 237 } else { 238 self.read_stack()? 239 }; 240 - Ok(fzf::select(stack)?.map(|si| si.id)) 241 } 242 243 pub fn reprioritize(&self, identifier: TaskIdentifier) -> Result<()> { ··· 261 pub file: Flock<File>, 262 } 263 264 impl Task { 265 /// Consumes a task and saves it to disk. 266 pub fn save(mut self) -> Result<()> { ··· 270 self.file.write_all(b"\n\n")?; 271 self.file.write_all(self.body.trim().as_bytes())?; 272 Ok(()) 273 } 274 }
··· 2 use nix::fcntl::{Flock, FlockArg}; 3 4 use crate::errors::{Error, Result}; 5 + use crate::stack::{StackItem, TaskStack}; 6 use crate::{fzf, util}; 7 + use std::collections::vec_deque; 8 use std::fmt::Display; 9 use std::fs::{self, File}; 10 use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; ··· 230 pub fn search( 231 &self, 232 stack: Option<TaskStack>, 233 + search_body: bool, 234 _include_archived: bool, 235 ) -> Result<Option<Id>> { 236 let stack = if let Some(stack) = stack { ··· 238 } else { 239 self.read_stack()? 240 }; 241 + if search_body { 242 + let loader = LazyTaskLoader { 243 + files: stack.into_iter(), 244 + workspace: self, 245 + }; 246 + // search the entirety of a task 247 + Ok(fzf::select(loader)?.map(|bt| bt.id)) 248 + } else { 249 + // just search the stack 250 + Ok(fzf::select(stack)?.map(|si| si.id)) 251 + } 252 } 253 254 pub fn reprioritize(&self, identifier: TaskIdentifier) -> Result<()> { ··· 272 pub file: Flock<File>, 273 } 274 275 + /// A task container without a file handle 276 + pub struct BareTask { 277 + pub id: Id, 278 + pub title: String, 279 + pub body: String, 280 + } 281 + 282 impl Task { 283 /// Consumes a task and saves it to disk. 284 pub fn save(mut self) -> Result<()> { ··· 288 self.file.write_all(b"\n\n")?; 289 self.file.write_all(self.body.trim().as_bytes())?; 290 Ok(()) 291 + } 292 + 293 + fn bare(self) -> BareTask { 294 + BareTask { 295 + id: self.id, 296 + title: self.title, 297 + body: self.body, 298 + } 299 + } 300 + } 301 + 302 + impl FromStr for BareTask { 303 + type Err = Error; 304 + 305 + fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { 306 + let (tsk_id, task_content) = s.split_once('\t').ok_or(Error::Parse(format!( 307 + "Missing TSK-ID or content or task parse." 308 + )))?; 309 + let (title, body) = task_content 310 + .split_once('\t') 311 + .ok_or(Error::Parse(format!("Missing body for task parse.")))?; 312 + Ok(Self { 313 + id: tsk_id.parse()?, 314 + title: title.to_string(), 315 + body: body.to_string(), 316 + }) 317 + } 318 + } 319 + 320 + impl Display for BareTask { 321 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 322 + write!( 323 + f, 324 + "{}\t{}\t{}", 325 + self.id, 326 + self.title.trim(), 327 + self.body.replace('\n', " ").replace('\r', "") 328 + ) 329 + } 330 + } 331 + 332 + struct LazyTaskLoader<'a> { 333 + files: vec_deque::IntoIter<StackItem>, 334 + workspace: &'a Workspace, 335 + } 336 + 337 + impl<'a> Iterator for LazyTaskLoader<'a> { 338 + type Item = BareTask; 339 + 340 + fn next(&mut self) -> Option<Self::Item> { 341 + let stack_item = self.files.next()?; 342 + let task = self 343 + .workspace 344 + .task(TaskIdentifier::Id(stack_item.id)) 345 + .ok()?; 346 + Some(task.bare()) 347 + } 348 + } 349 + 350 + #[cfg(test)] 351 + mod test { 352 + use super::*; 353 + 354 + #[test] 355 + fn test_bare_task_display() { 356 + let task = BareTask { 357 + id: Id(123), 358 + title: "Hello, world".to_string(), 359 + body: "The body of the task.\nAnother line\r\nis here.".to_string(), 360 + }; 361 + assert_eq!( 362 + "tsk-123\tHello, world\tThe body of the task. Another line is here.", 363 + task.to_string() 364 + ); 365 } 366 }