A file-based task manager

ADD: completion and edit commands

+65 -16
+34 -9
src/main.rs
··· 2 2 mod stack; 3 3 mod util; 4 4 mod workspace; 5 + use clap_complete::{generate, Shell}; 6 + use std::io; 5 7 use std::path::PathBuf; 6 8 use std::{env::current_dir, io::Read}; 7 - use clap_complete::Shell; 8 9 use workspace::Workspace; 9 10 10 11 //use smol; 11 12 //use iocraft::prelude::*; 12 - use clap::{Args, Parser, Subcommand}; 13 + use clap::{Args, CommandFactory, Parser, Subcommand}; 13 14 use edit::edit as open_editor; 14 15 15 16 fn default_dir() -> PathBuf { ··· 59 60 60 61 Edit { 61 62 #[arg(short = 't')] 62 - task_id: u32 63 + task_id: u32, 63 64 }, 64 65 65 66 Completion { 66 67 #[arg(short = 's')] 67 - shell: Shell 68 + shell: Shell, 69 + }, 70 + 71 + /* 72 + Drop { 73 + #[arg(short = 't')] 74 + task_id: Option<u32>, 68 75 } 76 + */ 69 77 } 70 78 71 79 #[derive(Args)] ··· 88 96 command_push(cli.dir.unwrap_or(default_dir()), edit, body, title) 89 97 } 90 98 Commands::List { all, count } => command_list(cli.dir.unwrap_or(default_dir()), all, count), 91 - Commands::Swap => command_swap(cli.dir.unwrap_or(default_dir())) 99 + Commands::Swap => command_swap(cli.dir.unwrap_or(default_dir())), 100 + Commands::Edit { task_id } => command_edit(cli.dir.unwrap_or(default_dir()), task_id), 101 + Commands::Completion { shell } => command_completion(shell), 92 102 } 93 103 } 94 104 ··· 98 108 99 109 fn command_push(dir: PathBuf, edit: bool, body: Option<String>, title: Title) { 100 110 let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 101 - let title = if let Some(title) = title.title { 111 + let mut title = if let Some(title) = title.title { 102 112 title 103 113 } else if let Some(title) = title.title_simple { 104 114 let joined = title.join(" "); ··· 116 126 .expect("Failed to read stdin"); 117 127 } 118 128 if edit { 119 - body = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file"); 129 + let new_content = open_editor(format!("{title}\n\n{body}")).expect("Failed to edit file"); 130 + if let Some(content) = new_content.split_once("\n") { 131 + title = content.0.to_string(); 132 + body = content.1.to_string(); 133 + } 120 134 } 121 135 let task = workspace 122 136 .new_task(title, body) ··· 147 161 workspace.swap_top().expect("swap to work"); 148 162 } 149 163 150 - fn command_edit(dir: PathBuf) { 164 + fn command_edit(dir: PathBuf, id: u32) { 151 165 let workspace = Workspace::from_path(dir).expect("Unable to find .tsk dir"); 152 - let task = workspace. 166 + let mut task = workspace.task(id.into()).expect("To read task from disk"); 167 + let new_content = 168 + open_editor(format!("{}\n\n{}", task.title.trim(), task.body.trim())).expect("Failed to edit file"); 169 + if let Some((title, body)) = new_content.split_once("\n") { 170 + task.title = title.to_string(); 171 + task.body = body.to_string(); 172 + task.save().expect("Failed to save task"); 173 + } 174 + } 175 + 176 + fn command_completion(shell: Shell) { 177 + generate(shell, &mut Cli::command(), "tsk", &mut io::stdout()) 153 178 }
+31 -7
src/workspace.rs
··· 6 6 use crate::util; 7 7 use std::fmt::Display; 8 8 use std::fs::File; 9 - use std::io::{Read, Seek}; 9 + use std::io::{BufRead as _, BufReader, Read, Seek, SeekFrom}; 10 10 use std::path::PathBuf; 11 11 use std::str::FromStr; 12 12 use std::{fs::OpenOptions, io::Write}; ··· 30 30 impl Display for Id { 31 31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 32 write!(f, "tsk-{}", self.0) 33 + } 34 + } 35 + 36 + impl From<u32> for Id { 37 + fn from(value: u32) -> Self { 38 + Id(value) 33 39 } 34 40 } 35 41 ··· 82 88 // reset the files contents 83 89 file.set_len(0)?; 84 90 // TODO: figure out if this is necessary 85 - file.seek(std::io::SeekFrom::Start(0))?; 91 + file.seek(SeekFrom::Start(0))?; 86 92 // store the *next* if 87 93 file.write_all(format!("{}\n", id + 1).as_bytes())?; 88 94 Ok(Id(id)) ··· 105 111 } 106 112 107 113 pub fn task(&self, id: Id) -> Result<Task> { 108 - let mut file = util::flopen( 114 + let file = util::flopen( 109 115 self.path.join("tasks").join(format!("tsk-{}.tsk", id.0)), 110 116 FlockArg::LockExclusive, 111 117 )?; 112 - 118 + let mut title = String::new(); 119 + let mut body = String::new(); 120 + let mut reader = BufReader::new(&*file); 121 + reader.read_line(&mut title)?; 122 + reader.read_to_string(&mut body)?; 123 + drop(reader); 124 + Ok(Task { 125 + id, 126 + title, 127 + body, 128 + file, 129 + }) 113 130 } 114 131 115 132 pub fn read_stack(&self, count: Option<usize>) -> Result<TaskStack> { ··· 138 155 pub file: Flock<File>, 139 156 } 140 157 141 - #[cfg(test)] 142 - mod test { 143 - fn test_next_id() {} 158 + impl Task { 159 + /// Consumes a task and saves it to disk. 160 + pub fn save(mut self) -> Result<()> { 161 + self.file.set_len(0)?; 162 + self.file.seek(SeekFrom::Start(0))?; 163 + self.file.write_all(self.title.trim().as_bytes())?; 164 + self.file.write_all(b"\n\n")?; 165 + self.file.write_all(self.body.trim().as_bytes())?; 166 + Ok(()) 167 + } 144 168 }